|
- """Verify promoted identities and clean up only this run's temporary avatars."""
- import hashlib
- import json
- import shlex
-
- from promote_instructors_v2 import connect, run, RELEASE
- from retrain_instructors_v2 import ROOT, REPORT, IDS, FILES, API, SERVICE, HTTP, api_session, save
-
-
- def main():
- jobs = json.loads((REPORT / 'jobs.json').read_text('utf-8'))
- checks = json.loads((REPORT / 'generated-validation.json').read_text('utf-8'))
- playback = json.loads((REPORT / 'stable-viewer-validation.json').read_text('utf-8'))
- catalog = json.loads((REPORT / 'catalog-validation.json').read_text('utf-8'))
- assert len(playback) == catalog['count'] == 3
- assert all(not item['errors'] and item['driver']['active'] > 30 for item in playback)
- session = api_session()
- response = session.get(API + '/api/v1/avatars', params={'pageSize': 200}, timeout=30)
- response.raise_for_status()
- after = response.json()
- save('after-platform.json', after)
- before = json.loads((REPORT / 'before-platform.json').read_text('utf-8'))
- # No full catalog sync: it would re-import previously soft-deleted legacy entries.
- assert before['data']['total'] == after['data']['total'] == 3
- before_items = {item['id']: item for item in before['data']['items']}
- after_items = {item['id']: item for item in after['data']['items']}
- assert set(before_items) == set(after_items) == set(IDS)
- preserved_fields = ('id', 'name', 'status', 'default', 'voice', 'voiceCapabilityId', 'ownerId', 'visibility', 'viewerUrl')
- for ident in IDS:
- assert all(before_items[ident][field] == after_items[ident][field] for field in preserved_fields)
- service_after = []
- for check in checks:
- ident = check['stable_id']
- metadata = HTTP.get(SERVICE + '/api/v1/avatars/' + ident, timeout=30)
- metadata.raise_for_status()
- service_after.append(metadata.json())
- assert metadata.json()['status'] == 'ready'
- for filename in FILES:
- response = HTTP.get(SERVICE + f'/api/v1/avatars/{ident}/assets/{filename}', timeout=45)
- response.raise_for_status()
- assert hashlib.sha256(response.content).hexdigest() == check['sha256'][filename]
- save('after-service.json', service_after)
-
- client = connect()
- try:
- expected = {row['stable_id']: row['sha256'] for row in checks}
- 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()}))"
- actual = json.loads(run(client, 'docker run --rm --network none --entrypoint python digital-human-service:instructors-v2-20260914 -c ' + shlex.quote(code)))
- assert actual == expected
- save('image-validation.json', actual)
- # The list uses source-delivered PNG covers, so replace those exact assets too.
- run(client, f'mkdir -p {RELEASE}/backup-covers/source {RELEASE}/backup-covers/published')
- with client.open_sftp() as sftp:
- for number in (1, 2, 3):
- name = f'equipment-instructor-{number}.png'
- local = ROOT / 'ai_person/ai_person_web/public/avatars/builtin' / name
- for group, base in (
- ('source', '/home/ai_person/ai_person_web/public/avatars/builtin'),
- ('published', '/home/ai_person/html_ap/avatars/builtin'),
- ):
- target = base + '/' + name
- backup = RELEASE + '/backup-covers/' + group + '/' + name
- run(client, f'test -f {target} && test ! -e {backup} && cp {target} {backup}')
- sftp.put(str(local), target + '.v2-stage')
- sftp.posix_rename(target + '.v2-stage', target)
- with sftp.open(target, 'rb') as stream:
- assert hashlib.sha256(stream.read()).digest() == hashlib.sha256(local.read_bytes()).digest()
- # Keep service-side generation logs in the rollback/audit report before deleting temporary jobs.
- with client.open_sftp() as sftp:
- for job in jobs:
- sftp.get('/home/digital-human-service/data/jobs/' + job['job_id'] + '.log', str(REPORT / f"generation-{job['number']}.log"))
- finally:
- client.close()
- removed = []
- for job in jobs:
- assert job['avatar_id'] not in IDS
- response = HTTP.delete(SERVICE + '/api/v1/avatars/' + job['avatar_id'], timeout=45)
- assert response.status_code == 204, response.status_code
- removed.append(job['avatar_id'])
- save('final-validation.json', {
- 'identitiesPreserved': True, 'defaultInstructor': 2, 'voicesPreserved': True,
- 'platformCount': 3, 'runtimeAssetHashesMatch': True, 'imageAssetHashesMatch': True,
- 'localAndPublishedCoversUpdated': True, 'temporaryAvatarsRemoved': removed,
- 'oldLectureVideosAndAudioPreserved': True,
- 'scope': 'Resource regeneration and runtime playback verified; phoneme-level lip sync quality not accepted by this check.',
- })
- print('Verified: three original identities, default instructor, voices, runtime/image assets and covers. Temporary generation entries removed.')
-
-
- if __name__ == '__main__':
- main()
|