"""Prepare v1.4.28 package QA only; never build or alter product files.""" from pathlib import Path import ast import hashlib import json import shutil folder = Path(__file__).resolve().parent baseline = folder.parent / 'review-voice-20260918' root = Path('C:/wechat_rpa') old_sha = '1ef5f2155992423cdb0ac3d3c0f29bcbd5f401baae13d359a98292d02c036fff' old = root / 'releases/甄AI客服-安装包-v1.4.27.exe' assert hashlib.sha256(old.read_bytes()).hexdigest() == old_sha mapping = { 'build_review_voice_v1_4_27.ps1': 'build_contact_recovery_v1_4_28.ps1', 'build_windows_cached_review_voice.ps1': 'build_windows_cached_contact_recovery.ps1', 'validate_review_voice_v1_4_27.ps1': 'validate_contact_recovery_v1_4_28.ps1', 'verify_review_voice_frozen.py': 'verify_contact_recovery_frozen.py', 'run_review_voice_frozen_regressions.py': 'run_contact_recovery_frozen_regressions.py', 'run_isolated_review_voice.py': 'run_isolated_contact_recovery.py', 'verify_review_voice_nsis.py': 'verify_contact_recovery_nsis.py', 'verify_frozen_review_voice_notifications.py': 'verify_frozen_contact_recovery_notifications.py', 'verify_frozen_offline_asr.py': 'verify_frozen_offline_asr.py', } for source_name, destination_name in mapping.items(): content = (baseline / source_name).read_text(encoding='utf-8-sig') for before, after in mapping.items(): content = content.replace(before, after) content = content.replace('1.4.27', '1.4.28').replace('1.4.26', '1.4.27') content = content.replace('a558c24f7db00ec907be46f8ce67f2d9d7bb1f60992306bfa64020e294818a38', old_sha) content = content.replace('review-voice', 'contact-recovery').replace('review and voice release', 'contact recovery release') if source_name == 'build_review_voice_v1_4_27.ps1': marker = "$taskPython = Join-Path $taskRoot '.build-venv\\Scripts\\python.exe'" content = content.replace(marker, "if (([IO.DriveInfo]::new('C:\\')).AvailableFreeSpace -lt 3500000000) { throw 'At least 3.5 GB free disk space required; do not delete user data to build' }\n" + marker) content = content.replace("rapidocr_onnxruntime; from Crypto.Cipher import AES", "rapidocr_onnxruntime,faster_whisper,ctranslate2,av,pysilk; from Crypto.Cipher import AES") if source_name in {'build_review_voice_v1_4_27.ps1', 'verify_review_voice_frozen.py', 'run_review_voice_frozen_regressions.py'}: content = content.replace("'wechat_gui_qt','app_version'", "'wechat_gui_qt','app_version','contact_reply_guard','contact_relation'") if source_name == 'validate_review_voice_v1_4_27.ps1': marker = "& $taskPython -B (Join-Path $PSScriptRoot 'verify_contact_recovery_nsis.py')" content = content.replace(marker, "& $taskPython -I -S (Join-Path $PSScriptRoot 'verify_frozen_offline_asr.py')\nif ($LASTEXITCODE -ne 0) { throw 'Actual frozen offline ASR verification failed' }\n" + marker) destination = folder / destination_name if destination.exists(): raise FileExistsError(destination) destination.write_text(content, encoding='utf-8-sig' if destination.suffix == '.ps1' else 'utf-8') if destination.suffix == '.py': ast.parse(content, filename=str(destination)) plan = json.loads((baseline / 'review-voice-release-plan.json').read_text(encoding='utf-8-sig')) plan.update(finalized=False, version='1.4.28', previousVersion='1.4.27', previousInstallerSha256=old_sha) plan['frozenModules'].append('contact_relation') plan['windowsTestModules'] = ['test_protocol_contact_recovery', 'test_contact_relation_snapshot', *plan['windowsTestModules']] plan['notes'] = 'Prepare only. Root must finish, test and freeze contact re-add reception fixes before finalized=true. Reuse installed ASR dependencies/assets; no downloads. No live messages, settings, queues, installation or uploads.' (folder / 'contact-recovery-release-plan.json').write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding='utf-8') for extension in ('wav', 'silk', 'amr'): shutil.copy2(baseline / f'synthetic-chinese.{extension}', folder / f'synthetic-chinese.{extension}') report = dict(prepared=True, built=False, productFilesChanged=False, version='1.4.28', previousInstaller=str(old), previousInstallerBytes=old.stat().st_size, previousInstallerSha256=old_sha, freeDiskBytes=shutil.disk_usage(root).free, requiredFreeDiskBytes=3500000000, dependenciesInstalled=False, modelsDownloaded=False, installed=False, realMessagesSent=0, files=list(mapping.values())) (folder / 'package-preparation.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8') print(json.dumps(report, ensure_ascii=False))