/** * C02-C15 remote full-page audio manifest v1. * * Import contract for a future reviewed batch: * * 'S01-C09-P05': Object.freeze({ * pageId: 'S01-C09-P05', * chapterNumber: 9, * pageNumber: 5, * assetId: 'audio.page.s01.c09.p05', * durationSeconds: 42.1, * reviewStatus: 'approved', * }) * * The matching releaseAssetManifest entry must independently be an approved, * remote-only, immutable hash-named audio asset. Automated QA, pending human * listening, and missing CDN records must never be relabelled as approved. */ const REMOTE_PAGE_AUDIO_MANIFEST_VERSION = 1 const REMOTE_PAGE_AUDIO_ID_PATTERN = /^S01-C(0[2-9]|1[0-5])-P(0[1-8])$/ const SHA256_PATTERN = /^[a-f0-9]{64}$/ // Deliberately empty until full-page tracks pass the required human reviews // and their immutable CDN objects are available. This is a release gate, not // a placeholder that the UI may treat as playable. const remotePageAudioPages = Object.freeze({}) function clean(value) { return typeof value === 'string' ? value.trim() : '' } function remotePageAudioAssetId(pageId) { const stablePageId = clean(pageId) const match = REMOTE_PAGE_AUDIO_ID_PATTERN.exec(stablePageId) if (!match) return '' return `audio.page.s01.c${match[1]}.p${match[2]}` } function getApprovedRemotePageAudio( pageId, releaseAssets = {}, pageManifest = remotePageAudioPages, ) { const stablePageId = clean(pageId) const match = REMOTE_PAGE_AUDIO_ID_PATTERN.exec(stablePageId) if (!match) return null const entry = pageManifest && pageManifest[stablePageId] if (!entry || typeof entry !== 'object') return null const chapterNumber = Number(match[1]) const pageNumber = Number(match[2]) const assetId = remotePageAudioAssetId(stablePageId) const durationSeconds = Number(entry.durationSeconds) if ( clean(entry.reviewStatus).toLowerCase() !== 'approved' || entry.pageId !== stablePageId || Number(entry.chapterNumber) !== chapterNumber || Number(entry.pageNumber) !== pageNumber || entry.assetId !== assetId || !Number.isFinite(durationSeconds) || durationSeconds <= 0 ) { return null } const asset = releaseAssets && releaseAssets[assetId] const sha256 = asset && typeof asset.sha256 === 'string' ? asset.sha256 : '' const remotePath = asset && asset.remotePath const expectedRemotePath = ( `audio/${stablePageId.slice(0, 7).toLowerCase()}` + `/${stablePageId}.${sha256.slice(0, 12)}.mp3` ) if ( !asset || asset.kind !== 'audio' || clean(asset.reviewStatus).toLowerCase() !== 'approved' || asset.localSeed !== '' || !SHA256_PATTERN.test(sha256) || typeof remotePath !== 'string' || remotePath !== expectedRemotePath ) { return null } return { ...entry, assetId } } module.exports = { REMOTE_PAGE_AUDIO_ID_PATTERN, REMOTE_PAGE_AUDIO_MANIFEST_VERSION, getApprovedRemotePageAudio, remotePageAudioAssetId, remotePageAudioPages, }