"""Regenerate revised lecture text with the approved v2 avatar; retain prior media.""" from pathlib import Path import hashlib import importlib.util import json import subprocess import threading import requests spec = importlib.util.spec_from_file_location('render_static', Path(__file__).with_name('bridge-digital-video-static-generate.py')) render = importlib.util.module_from_spec(spec) spec.loader.exec_module(render) ROOT, WEB = render.ROOT, render.WEB REPORT = ROOT / 'unreal_tran/ute2e/reports/bridge-lecture-polish-20260915' def read_manifest(name): command = f"import m from './src/features/virtual-training-scripts/{name}.js';console.log(JSON.stringify(m))" return json.loads(subprocess.check_output(['node', '--input-type=module', '-e', command], cwd=WEB, encoding='utf-8')) def main(): REPORT.mkdir(parents=True, exist_ok=True) speech, videos = read_manifest('narration-manifest'), read_manifest('narration-video-manifest') backup = REPORT / 'before-video-manifest.json' if not backup.exists(): render.write_json(backup, videos) settings = render.get_settings() avatar = videos['profile']['providerAvatarId'] http = requests.Session(); http.trust_env = False response = http.get(str(settings.service_base_url).rstrip('/') + f'/api/v1/avatars/{avatar}/assets/manifest.json', timeout=30) response.raise_for_status() assert hashlib.sha256(response.content).hexdigest() == videos['profile']['avatarManifestSha256'] progress_file = REPORT / 'regenerated.json' progress = json.loads(progress_file.read_text('utf-8')) if progress_file.exists() else [] worker = render.OpenGenerationWorker(None, settings, None, None) for pack in speech['scripts']: target = next(p for p in videos['scripts'] if p['id'] == pack['id'] and p['version'] == pack['version']) for clip in pack['steps']: previous = next(c for c in target['steps'] if c['stepCode'] == clip['stepCode']) assert previous['expectedTitle'] == clip['expectedTitle'] and previous['expectedInstruction'] == clip['expectedInstruction'] if previous['text'] == clip['text']: assert render.digest(WEB / 'public' / previous['url'].lstrip('/')) == previous['sha256'] continue audio = WEB / 'public' / clip['url'].lstrip('/') assert render.digest(audio) == clip['stepSha256'] signature = hashlib.sha256(json.dumps([videos['profile'], clip['stepSha256'], clip['text']], sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:16] url = f"/legacy/virtual-training/videos/narration/{pack['id']}/v{pack['version']}/instructor-1-v2/step-{clip['number']:02}-{signature}.mp4" output = WEB / 'public' / url.lstrip('/') entry = next((p for p in progress if p['url'] == url), None) if not entry or not output.exists() or render.digest(output) != entry['sha256']: print(f"Rendering {pack['code']} step {clip['number']}: {clip['text']}", flush=True) captured = REPORT / 'capture.webm' command = render.GenerationCommand(title=f"{pack['title']} · 第{clip['number']}步", text=clip['text'], avatarId='55', voiceId='49', speed=.85, width=480, height=640, subtitles=False) worker.capture(command, avatar, audio, None, captured, threading.Event()) subprocess.run([settings.ffmpeg_binary, '-hide_banner', '-loglevel', 'error', '-y', '-i', str(captured), '-r', '25', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', str(output)], check=True) duration = render.validate(settings, output, clip['stepDuration']) entry = {'code': pack['code'], 'stepCode': clip['stepCode'], 'number': clip['number'], 'text': clip['text'], 'url': url, 'sha256': render.digest(output), 'duration': duration, 'audioSha256': clip['stepSha256'], 'previousUrl': previous['url'], 'previousSha256': previous['sha256']} progress.append(entry); render.write_json(progress_file, progress) else: render.validate(settings, output, clip['stepDuration']) previous.update({key: entry[key] for key in ('text', 'url', 'sha256', 'duration')}) print(f"Verified {pack['code']} step {clip['number']}", flush=True) target = WEB / 'src/features/virtual-training-scripts/narration-video-manifest.js' temporary = target.with_suffix('.js.tmp') temporary.write_text('// Generated by bridge-digital-video-static-generate.py; revised by bridge-digital-video-refresh.py.\nexport default ' + json.dumps(videos, ensure_ascii=False, indent=2) + ';\n', encoding='utf-8') temporary.replace(target) render.write_json(REPORT / 'video-manifest.json', videos) render.write_json(REPORT / 'narration-manifest.json', speech) print(json.dumps({'regenerated': len(progress), 'total': sum(len(p['steps']) for p in videos['scripts']), 'previousFilesPreserved': True}), flush=True) if __name__ == '__main__': main()