選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

92 行
5.2 KiB

  1. """Verify promoted identities and clean up only this run's temporary avatars."""
  2. import hashlib
  3. import json
  4. import shlex
  5. from promote_instructors_v2 import connect, run, RELEASE
  6. from retrain_instructors_v2 import ROOT, REPORT, IDS, FILES, API, SERVICE, HTTP, api_session, save
  7. def main():
  8. jobs = json.loads((REPORT / 'jobs.json').read_text('utf-8'))
  9. checks = json.loads((REPORT / 'generated-validation.json').read_text('utf-8'))
  10. playback = json.loads((REPORT / 'stable-viewer-validation.json').read_text('utf-8'))
  11. catalog = json.loads((REPORT / 'catalog-validation.json').read_text('utf-8'))
  12. assert len(playback) == catalog['count'] == 3
  13. assert all(not item['errors'] and item['driver']['active'] > 30 for item in playback)
  14. session = api_session()
  15. response = session.get(API + '/api/v1/avatars', params={'pageSize': 200}, timeout=30)
  16. response.raise_for_status()
  17. after = response.json()
  18. save('after-platform.json', after)
  19. before = json.loads((REPORT / 'before-platform.json').read_text('utf-8'))
  20. # No full catalog sync: it would re-import previously soft-deleted legacy entries.
  21. assert before['data']['total'] == after['data']['total'] == 3
  22. before_items = {item['id']: item for item in before['data']['items']}
  23. after_items = {item['id']: item for item in after['data']['items']}
  24. assert set(before_items) == set(after_items) == set(IDS)
  25. preserved_fields = ('id', 'name', 'status', 'default', 'voice', 'voiceCapabilityId', 'ownerId', 'visibility', 'viewerUrl')
  26. for ident in IDS:
  27. assert all(before_items[ident][field] == after_items[ident][field] for field in preserved_fields)
  28. service_after = []
  29. for check in checks:
  30. ident = check['stable_id']
  31. metadata = HTTP.get(SERVICE + '/api/v1/avatars/' + ident, timeout=30)
  32. metadata.raise_for_status()
  33. service_after.append(metadata.json())
  34. assert metadata.json()['status'] == 'ready'
  35. for filename in FILES:
  36. response = HTTP.get(SERVICE + f'/api/v1/avatars/{ident}/assets/{filename}', timeout=45)
  37. response.raise_for_status()
  38. assert hashlib.sha256(response.content).hexdigest() == check['sha256'][filename]
  39. save('after-service.json', service_after)
  40. client = connect()
  41. try:
  42. expected = {row['stable_id']: row['sha256'] for row in checks}
  43. code = 'from pathlib import Path;import json,hashlib;expected=' + repr(expected) + ";print(json.dumps({ident:{name:hashlib.sha256((Path('/app/builtin_avatars')/ident/'assets'/name).read_bytes()).hexdigest() for name in files} for ident,files in expected.items()}))"
  44. actual = json.loads(run(client, 'docker run --rm --network none --entrypoint python digital-human-service:instructors-v2-20260914 -c ' + shlex.quote(code)))
  45. assert actual == expected
  46. save('image-validation.json', actual)
  47. # The list uses source-delivered PNG covers, so replace those exact assets too.
  48. run(client, f'mkdir -p {RELEASE}/backup-covers/source {RELEASE}/backup-covers/published')
  49. with client.open_sftp() as sftp:
  50. for number in (1, 2, 3):
  51. name = f'equipment-instructor-{number}.png'
  52. local = ROOT / 'ai_person/ai_person_web/public/avatars/builtin' / name
  53. for group, base in (
  54. ('source', '/home/ai_person/ai_person_web/public/avatars/builtin'),
  55. ('published', '/home/ai_person/html_ap/avatars/builtin'),
  56. ):
  57. target = base + '/' + name
  58. backup = RELEASE + '/backup-covers/' + group + '/' + name
  59. run(client, f'test -f {target} && test ! -e {backup} && cp {target} {backup}')
  60. sftp.put(str(local), target + '.v2-stage')
  61. sftp.posix_rename(target + '.v2-stage', target)
  62. with sftp.open(target, 'rb') as stream:
  63. assert hashlib.sha256(stream.read()).digest() == hashlib.sha256(local.read_bytes()).digest()
  64. # Keep service-side generation logs in the rollback/audit report before deleting temporary jobs.
  65. with client.open_sftp() as sftp:
  66. for job in jobs:
  67. sftp.get('/home/digital-human-service/data/jobs/' + job['job_id'] + '.log', str(REPORT / f"generation-{job['number']}.log"))
  68. finally:
  69. client.close()
  70. removed = []
  71. for job in jobs:
  72. assert job['avatar_id'] not in IDS
  73. response = HTTP.delete(SERVICE + '/api/v1/avatars/' + job['avatar_id'], timeout=45)
  74. assert response.status_code == 204, response.status_code
  75. removed.append(job['avatar_id'])
  76. save('final-validation.json', {
  77. 'identitiesPreserved': True, 'defaultInstructor': 2, 'voicesPreserved': True,
  78. 'platformCount': 3, 'runtimeAssetHashesMatch': True, 'imageAssetHashesMatch': True,
  79. 'localAndPublishedCoversUpdated': True, 'temporaryAvatarsRemoved': removed,
  80. 'oldLectureVideosAndAudioPreserved': True,
  81. 'scope': 'Resource regeneration and runtime playback verified; phoneme-level lip sync quality not accepted by this check.',
  82. })
  83. print('Verified: three original identities, default instructor, voices, runtime/image assets and covers. Temporary generation entries removed.')
  84. if __name__ == '__main__':
  85. main()