You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

122 line
5.2 KiB

  1. """Reproducible v2 instructor generation; reports contain no credentials."""
  2. from pathlib import Path
  3. import argparse
  4. import gzip
  5. import hashlib
  6. import json
  7. import re
  8. import shutil
  9. import requests
  10. ROOT = Path(__file__).resolve().parents[3]
  11. REPORT = ROOT / 'ai_person/ape2e/reports/instructors-v2-20260914'
  12. BUNDLES = ROOT / 'ai_person/ai_person_service/builtin_avatars'
  13. SERVICE = 'http://1.14.103.234:8123'
  14. API = 'http://127.0.0.1:8001'
  15. FILES = ('01.mp4', 'combined_data.json.gz', 'thumbnail.jpg', 'manifest.json')
  16. IDS = [f'a100000000000000000000000000000{i}' for i in (5, 6, 7)]
  17. HTTP = requests.Session()
  18. HTTP.trust_env = False
  19. def save(name, value):
  20. REPORT.mkdir(parents=True, exist_ok=True)
  21. (REPORT / name).write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding='utf-8')
  22. def api_session():
  23. values = {}
  24. for line in (ROOT / 'ai_person/账号.MD').read_text(encoding='utf-8-sig').splitlines():
  25. match = re.match(r'^\s*([^::]+)\s*[::]\s*(.*?)\s*$', line)
  26. if match:
  27. values[match[1].strip()] = match[2].strip()
  28. session = requests.Session()
  29. session.trust_env = False
  30. response = session.post(API + '/api/auth/v1/auth/login', json={
  31. 'roleCode': 'admin', 'username': values.get('账号') or values.get('用户名'),
  32. 'password': values['密码'], 'rememberMe': False,
  33. }, timeout=30)
  34. response.raise_for_status()
  35. data = response.json()['data']
  36. token = data.get('accessToken') or data['tokens']['accessToken']
  37. session.headers['Authorization'] = 'Bearer ' + token
  38. return session
  39. def get_json(url):
  40. response = HTTP.get(url, timeout=30)
  41. response.raise_for_status()
  42. return response.json()
  43. def snapshot():
  44. if (REPORT / 'before-platform.json').exists():
  45. raise RuntimeError('Backup already exists; refusing to overwrite')
  46. session = api_session()
  47. response = session.get(API + '/api/v1/avatars', params={'pageSize': 200}, timeout=30)
  48. response.raise_for_status()
  49. save('before-platform.json', response.json())
  50. save('before-service.json', [get_json(SERVICE + '/api/v1/avatars/' + ident) for ident in IDS])
  51. for ident in IDS:
  52. shutil.copytree(BUNDLES / ident, REPORT / 'backup-local' / ident)
  53. print('Backed up platform catalog, service metadata and all three local bundles')
  54. def submit():
  55. if not (REPORT / 'before-platform.json').exists():
  56. raise RuntimeError('Run snapshot first')
  57. state = json.loads((REPORT / 'jobs.json').read_text(encoding='utf-8')) if (REPORT / 'jobs.json').exists() else []
  58. for number, ident in enumerate(IDS, 1):
  59. if any(item['stable_id'] == ident for item in state):
  60. continue
  61. source = ROOT / '数字人素材' / f'教员{number}_v2.mp4'
  62. with source.open('rb') as stream:
  63. response = HTTP.post(SERVICE + '/api/v1/avatars',
  64. files={'video': (source.name, stream, 'video/mp4')},
  65. data={'name': f'教员{number} v2 重训 20260914', 'resize': 'true', 'matting': 'off'}, timeout=180)
  66. response.raise_for_status()
  67. item = {'number': number, 'stable_id': ident, 'source': source.name,
  68. 'source_sha256': hashlib.sha256(source.read_bytes()).hexdigest(), **response.json()}
  69. state.append(item)
  70. save('jobs.json', state)
  71. print(json.dumps(item, ensure_ascii=False), flush=True)
  72. def status():
  73. values = []
  74. for item in json.loads((REPORT / 'jobs.json').read_text(encoding='utf-8')):
  75. job = get_json(SERVICE + item['job_url'])
  76. values.append({'number': item['number'], **job})
  77. save('job-status.json', values)
  78. print(json.dumps(values, ensure_ascii=False, indent=2))
  79. def download():
  80. values = []
  81. for item in json.loads((REPORT / 'jobs.json').read_text(encoding='utf-8')):
  82. avatar = get_json(SERVICE + '/api/v1/avatars/' + item['avatar_id'])
  83. if avatar['status'] != 'ready':
  84. raise RuntimeError(f"Instructor {item['number']} not ready")
  85. target = REPORT / 'generated' / item['stable_id'] / 'assets'
  86. target.mkdir(parents=True, exist_ok=True)
  87. for filename in FILES:
  88. response = HTTP.get(SERVICE + f"/api/v1/avatars/{item['avatar_id']}/assets/{filename}", timeout=120)
  89. response.raise_for_status()
  90. (target / filename).write_bytes(response.content)
  91. manifest = json.loads((target / 'manifest.json').read_text(encoding='utf-8'))
  92. for filename, length in manifest['files'].items():
  93. assert (target / filename).stat().st_size == length, filename
  94. with gzip.open(target / 'combined_data.json.gz', 'rt', encoding='utf-8') as stream:
  95. geometry = json.load(stream)
  96. values.append({'number': item['number'], 'stable_id': item['stable_id'], 'generated_id': item['avatar_id'],
  97. 'manifest': manifest, 'geometry_type': type(geometry).__name__,
  98. 'sha256': {name: hashlib.sha256((target / name).read_bytes()).hexdigest() for name in FILES}})
  99. save('generated-validation.json', values)
  100. print(json.dumps(values, ensure_ascii=False, indent=2))
  101. if __name__ == '__main__':
  102. parser = argparse.ArgumentParser()
  103. parser.add_argument('phase', choices=['snapshot', 'submit', 'status', 'download'])
  104. globals()[parser.parse_args().phase]()