Files
xuetang/TUICallKit-Vue3/native/tang-detective/package-audio-player/data/remotePageAudioManifest.js
T
2026-09-09 14:47:29 +08:00

98 lines
2.9 KiB
JavaScript

/**
* 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,
}