Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 

78 рядки
5.1 KiB

  1. """Regenerate revised lecture text with the approved v2 avatar; retain prior media."""
  2. from pathlib import Path
  3. import hashlib
  4. import importlib.util
  5. import json
  6. import subprocess
  7. import threading
  8. import requests
  9. spec = importlib.util.spec_from_file_location('render_static', Path(__file__).with_name('bridge-digital-video-static-generate.py'))
  10. render = importlib.util.module_from_spec(spec)
  11. spec.loader.exec_module(render)
  12. ROOT, WEB = render.ROOT, render.WEB
  13. REPORT = ROOT / 'unreal_tran/ute2e/reports/bridge-lecture-polish-20260915'
  14. def read_manifest(name):
  15. command = f"import m from './src/features/virtual-training-scripts/{name}.js';console.log(JSON.stringify(m))"
  16. return json.loads(subprocess.check_output(['node', '--input-type=module', '-e', command], cwd=WEB, encoding='utf-8'))
  17. def main():
  18. REPORT.mkdir(parents=True, exist_ok=True)
  19. speech, videos = read_manifest('narration-manifest'), read_manifest('narration-video-manifest')
  20. backup = REPORT / 'before-video-manifest.json'
  21. if not backup.exists(): render.write_json(backup, videos)
  22. settings = render.get_settings()
  23. avatar = videos['profile']['providerAvatarId']
  24. http = requests.Session(); http.trust_env = False
  25. response = http.get(str(settings.service_base_url).rstrip('/') + f'/api/v1/avatars/{avatar}/assets/manifest.json', timeout=30)
  26. response.raise_for_status()
  27. assert hashlib.sha256(response.content).hexdigest() == videos['profile']['avatarManifestSha256']
  28. progress_file = REPORT / 'regenerated.json'
  29. progress = json.loads(progress_file.read_text('utf-8')) if progress_file.exists() else []
  30. worker = render.OpenGenerationWorker(None, settings, None, None)
  31. for pack in speech['scripts']:
  32. target = next(p for p in videos['scripts'] if p['id'] == pack['id'] and p['version'] == pack['version'])
  33. for clip in pack['steps']:
  34. previous = next(c for c in target['steps'] if c['stepCode'] == clip['stepCode'])
  35. assert previous['expectedTitle'] == clip['expectedTitle'] and previous['expectedInstruction'] == clip['expectedInstruction']
  36. if previous['text'] == clip['text']:
  37. assert render.digest(WEB / 'public' / previous['url'].lstrip('/')) == previous['sha256']
  38. continue
  39. audio = WEB / 'public' / clip['url'].lstrip('/')
  40. assert render.digest(audio) == clip['stepSha256']
  41. signature = hashlib.sha256(json.dumps([videos['profile'], clip['stepSha256'], clip['text']], sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:16]
  42. url = f"/legacy/virtual-training/videos/narration/{pack['id']}/v{pack['version']}/instructor-1-v2/step-{clip['number']:02}-{signature}.mp4"
  43. output = WEB / 'public' / url.lstrip('/')
  44. entry = next((p for p in progress if p['url'] == url), None)
  45. if not entry or not output.exists() or render.digest(output) != entry['sha256']:
  46. print(f"Rendering {pack['code']} step {clip['number']}: {clip['text']}", flush=True)
  47. captured = REPORT / 'capture.webm'
  48. command = render.GenerationCommand(title=f"{pack['title']} · 第{clip['number']}步", text=clip['text'],
  49. avatarId='55', voiceId='49', speed=.85, width=480, height=640, subtitles=False)
  50. worker.capture(command, avatar, audio, None, captured, threading.Event())
  51. subprocess.run([settings.ffmpeg_binary, '-hide_banner', '-loglevel', 'error', '-y', '-i', str(captured),
  52. '-r', '25', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p',
  53. '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', str(output)], check=True)
  54. duration = render.validate(settings, output, clip['stepDuration'])
  55. entry = {'code': pack['code'], 'stepCode': clip['stepCode'], 'number': clip['number'], 'text': clip['text'],
  56. 'url': url, 'sha256': render.digest(output), 'duration': duration, 'audioSha256': clip['stepSha256'],
  57. 'previousUrl': previous['url'], 'previousSha256': previous['sha256']}
  58. progress.append(entry); render.write_json(progress_file, progress)
  59. else: render.validate(settings, output, clip['stepDuration'])
  60. previous.update({key: entry[key] for key in ('text', 'url', 'sha256', 'duration')})
  61. print(f"Verified {pack['code']} step {clip['number']}", flush=True)
  62. target = WEB / 'src/features/virtual-training-scripts/narration-video-manifest.js'
  63. temporary = target.with_suffix('.js.tmp')
  64. 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')
  65. temporary.replace(target)
  66. render.write_json(REPORT / 'video-manifest.json', videos)
  67. render.write_json(REPORT / 'narration-manifest.json', speech)
  68. print(json.dumps({'regenerated': len(progress), 'total': sum(len(p['steps']) for p in videos['scripts']), 'previousFilesPreserved': True}), flush=True)
  69. if __name__ == '__main__': main()