Вы не можете выбрать более 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()