diff --git a/content/public/asset-manifest.json b/content/public/asset-manifest.json index ae9a757..cd4bf11 100644 --- a/content/public/asset-manifest.json +++ b/content/public/asset-manifest.json @@ -47,6 +47,54 @@ "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5" + }, + { + "id": "soundtrack-afterglow-opus", + "fileName": "nightdrive-afterglow.opus", + "sha256": "693215397e8ae4c2c13a4a6e4cc74b0059fb26b396c7f35d99e0e2c3e738d4b1", + "mediaType": "audio/ogg", + "width": null, + "height": null, + "durationMs": 25123, + "rightsHolder": "Luca Pîslaru", + "license": "All rights reserved", + "attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5" + }, + { + "id": "soundtrack-afterglow-mp3", + "fileName": "nightdrive-afterglow.mp3", + "sha256": "685687ad131d61c34e42bcee29af16a4c8180d788c67d75f2331fa9082a3a44e", + "mediaType": "audio/mpeg", + "width": null, + "height": null, + "durationMs": 25116, + "rightsHolder": "Luca Pîslaru", + "license": "All rights reserved", + "attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5" + }, + { + "id": "soundtrack-drive-lift-opus", + "fileName": "nightdrive-drive-lift.opus", + "sha256": "d580e58039c9d700599cf100c96b50d4b5816d93a0af738a93a1e8646fbb7566", + "mediaType": "audio/ogg", + "width": null, + "height": null, + "durationMs": 11169, + "rightsHolder": "Luca Pîslaru", + "license": "All rights reserved", + "attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5" + }, + { + "id": "soundtrack-drive-lift-mp3", + "fileName": "nightdrive-drive-lift.mp3", + "sha256": "edcb802938d3d1e53392f9ccd2e153f72a24b494c205ce6f556d73de5fe709ec", + "mediaType": "audio/mpeg", + "width": null, + "height": null, + "durationMs": 11163, + "rightsHolder": "Luca Pîslaru", + "license": "All rights reserved", + "attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5" } ] } diff --git a/content/public/portfolio-content.json b/content/public/portfolio-content.json index 1405688..d85b725 100644 --- a/content/public/portfolio-content.json +++ b/content/public/portfolio-content.json @@ -253,6 +253,14 @@ "main": { "opusAssetId": "soundtrack-main-opus", "mp3AssetId": "soundtrack-main-mp3" + }, + "afterglow": { + "opusAssetId": "soundtrack-afterglow-opus", + "mp3AssetId": "soundtrack-afterglow-mp3" + }, + "driveLift": { + "opusAssetId": "soundtrack-drive-lift-opus", + "mp3AssetId": "soundtrack-drive-lift-mp3" } }, "sceneDescription": "A restrained retro-futuristic night drive supports the reading journey while the portfolio content remains fully available as a semantic document." diff --git a/inputs/prepared-assets/nightdrive-afterglow.mp3 b/inputs/prepared-assets/nightdrive-afterglow.mp3 new file mode 100644 index 0000000..342cd20 Binary files /dev/null and b/inputs/prepared-assets/nightdrive-afterglow.mp3 differ diff --git a/inputs/prepared-assets/nightdrive-afterglow.opus b/inputs/prepared-assets/nightdrive-afterglow.opus new file mode 100644 index 0000000..e20cffc Binary files /dev/null and b/inputs/prepared-assets/nightdrive-afterglow.opus differ diff --git a/inputs/prepared-assets/nightdrive-drive-lift.mp3 b/inputs/prepared-assets/nightdrive-drive-lift.mp3 new file mode 100644 index 0000000..dcf8598 Binary files /dev/null and b/inputs/prepared-assets/nightdrive-drive-lift.mp3 differ diff --git a/inputs/prepared-assets/nightdrive-drive-lift.opus b/inputs/prepared-assets/nightdrive-drive-lift.opus new file mode 100644 index 0000000..a13bb7a Binary files /dev/null and b/inputs/prepared-assets/nightdrive-drive-lift.opus differ diff --git a/scripts/check-bundle-budgets.mjs b/scripts/check-bundle-budgets.mjs index 1a5b5e5..8d9be79 100644 --- a/scripts/check-bundle-budgets.mjs +++ b/scripts/check-bundle-budgets.mjs @@ -76,7 +76,12 @@ for (const group of content.toolkit ?? []) for (const technology of group.techno } if (content.metadata?.socialPreviewAssetId) assetClassById.set(content.metadata.socialPreviewAssetId, 'project-still'); if (content.soundtrack) { - for (const region of [content.soundtrack.intro, content.soundtrack.main]) { + for (const region of [ + content.soundtrack.intro, + content.soundtrack.main, + content.soundtrack.afterglow, + content.soundtrack.driveLift, + ]) { if (region?.opusAssetId) assetClassById.set(region.opusAssetId, 'soundtrack-audio'); if (region?.mp3AssetId) assetClassById.set(region.mp3AssetId, 'soundtrack-audio'); } diff --git a/scripts/lib/bundle-budgets.mjs b/scripts/lib/bundle-budgets.mjs index 662d0fa..a600d8e 100644 --- a/scripts/lib/bundle-budgets.mjs +++ b/scripts/lib/bundle-budgets.mjs @@ -13,7 +13,7 @@ export const PERFORMANCE_BUDGETS = Object.freeze({ deferredProjectVisuals: 2.5 * 1024 * 1024, highTierExtras: 750 * 1024, deferredNonVideo: 3.5 * 1024 * 1024, - deferredAudio: 13 * 1024 * 1024, + deferredAudio: 16 * 1024 * 1024, imageSceneConcurrency: 2, videoConcurrency: 1, mobileTextureDimension: 2048, @@ -44,7 +44,7 @@ export const classifyArtifact = (path, source = '') => { if (/^assets\/project-[^/]+\.(?:webm|mp4)$/u.test(path)) return 'project-video'; if (/^assets\/project-[^/]*chart[^/]*\.(?:avif|webp|png|jpe?g)$/u.test(path)) return 'chart'; if (/^assets\/project-[^/]+\.(?:avif|webp|png|jpe?g|svg)$/u.test(path)) return 'project-still'; - if (/^assets\/nightdrive-(?:intro|main)\.(?:opus|mp3)$/u.test(path)) return 'soundtrack-audio'; + if (/^assets\/nightdrive-(?:intro|main|afterglow|drive-lift)\.(?:opus|mp3)$/u.test(path)) return 'soundtrack-audio'; if (/^(?:robots\.txt|sitemap\.xml|_headers\.example|_redirects\.example)$/u.test(path)) return 'release-metadata'; throw new Error(`Artifact is unclassified: ${path}`); }; diff --git a/scripts/materialize-empty-asset-set.mjs b/scripts/materialize-empty-asset-set.mjs index dd2208b..9cedb60 100644 --- a/scripts/materialize-empty-asset-set.mjs +++ b/scripts/materialize-empty-asset-set.mjs @@ -132,7 +132,7 @@ const assetReferences = (content) => { if (typeof content.soundtrack !== 'object' || content.soundtrack === null || Array.isArray(content.soundtrack)) { throw new Error('Portfolio soundtrack must be an object.'); } - for (const regionName of ['intro', 'main']) { + for (const regionName of ['intro', 'main', 'afterglow', 'driveLift']) { const region = content.soundtrack[regionName]; if (typeof region !== 'object' || region === null || Array.isArray(region)) { throw new Error(`Portfolio soundtrack ${regionName} must be an object.`); diff --git a/src/content/schemas.ts b/src/content/schemas.ts index ff09b30..04dff9b 100644 --- a/src/content/schemas.ts +++ b/src/content/schemas.ts @@ -202,6 +202,8 @@ const portfolioContentSchema = z.object({ soundtrack: z.object({ intro: soundtrackAssetSourcesSchema, main: soundtrackAssetSourcesSchema, + afterglow: soundtrackAssetSourcesSchema, + driveLift: soundtrackAssetSourcesSchema, }).strict().optional(), sceneDescription: visibleString, }).strict().superRefine((content, context) => { @@ -321,6 +323,10 @@ export const parseProductionInputSnapshot = (input: unknown): ProductionInputs = content.soundtrack.intro.mp3AssetId, content.soundtrack.main.opusAssetId, content.soundtrack.main.mp3AssetId, + content.soundtrack.afterglow.opusAssetId, + content.soundtrack.afterglow.mp3AssetId, + content.soundtrack.driveLift.opusAssetId, + content.soundtrack.driveLift.mp3AssetId, ]), ]; const seenReferences = new Set(); diff --git a/src/content/types.ts b/src/content/types.ts index a83809a..144cc3c 100644 --- a/src/content/types.ts +++ b/src/content/types.ts @@ -141,6 +141,8 @@ export interface SoundtrackAssetSources { export interface SoundtrackContent { intro: SoundtrackAssetSources; main: SoundtrackAssetSources; + afterglow: SoundtrackAssetSources; + driveLift: SoundtrackAssetSources; } export interface PortfolioContent { diff --git a/tests/integration/materialize-empty-asset-set.test.mjs b/tests/integration/materialize-empty-asset-set.test.mjs index 82b574a..7002dd1 100644 --- a/tests/integration/materialize-empty-asset-set.test.mjs +++ b/tests/integration/materialize-empty-asset-set.test.mjs @@ -64,6 +64,10 @@ describe('approved asset publication closure', () => { ['soundtrack-intro-mp3', 'nightdrive-intro.mp3'], ['soundtrack-main-opus', 'nightdrive-main.opus'], ['soundtrack-main-mp3', 'nightdrive-main.mp3'], + ['soundtrack-afterglow-opus', 'nightdrive-afterglow.opus'], + ['soundtrack-afterglow-mp3', 'nightdrive-afterglow.mp3'], + ['soundtrack-drive-lift-opus', 'nightdrive-drive-lift.opus'], + ['soundtrack-drive-lift-mp3', 'nightdrive-drive-lift.mp3'], ].map(([id, fileName], index) => { const bytes = Buffer.from(`approved soundtrack asset ${index}`); return { id, fileName, bytes, sha256: createHash('sha256').update(bytes).digest('hex') }; @@ -78,6 +82,8 @@ describe('approved asset publication closure', () => { soundtrack: { intro: { opusAssetId: 'soundtrack-intro-opus', mp3AssetId: 'soundtrack-intro-mp3' }, main: { opusAssetId: 'soundtrack-main-opus', mp3AssetId: 'soundtrack-main-mp3' }, + afterglow: { opusAssetId: 'soundtrack-afterglow-opus', mp3AssetId: 'soundtrack-afterglow-mp3' }, + driveLift: { opusAssetId: 'soundtrack-drive-lift-opus', mp3AssetId: 'soundtrack-drive-lift-mp3' }, }, })); await mkdir(join(root, 'inputs/prepared-assets'), { recursive: true }); diff --git a/tests/integration/public-content.test.ts b/tests/integration/public-content.test.ts index 28e7ea0..2bec1a3 100644 --- a/tests/integration/public-content.test.ts +++ b/tests/integration/public-content.test.ts @@ -47,6 +47,10 @@ describe('approved public portfolio projection', () => { content.soundtrack.intro.mp3AssetId, content.soundtrack.main.opusAssetId, content.soundtrack.main.mp3AssetId, + content.soundtrack.afterglow.opusAssetId, + content.soundtrack.afterglow.mp3AssetId, + content.soundtrack.driveLift.opusAssetId, + content.soundtrack.driveLift.mp3AssetId, ]), ]; expect([...publishedReferences].sort()).toEqual(manifest.assets.map((asset) => asset.id).sort()); diff --git a/tests/integration/verify-artifacts.test.ts b/tests/integration/verify-artifacts.test.ts index a95a21f..0912114 100644 --- a/tests/integration/verify-artifacts.test.ts +++ b/tests/integration/verify-artifacts.test.ts @@ -118,6 +118,28 @@ describe('artifact verification', () => { await expect(verifyArtifactDirectory(directory, inputs, contract)).resolves.toBeUndefined(); }); + it.each([ + { + name: 'an omitted adaptive soundtrack file', + mutate: async (directory: string) => rm(join(directory, 'assets/nightdrive-afterglow.opus')), + error: /missing.*artifact|digest|public asset/iu, + }, + { + name: 'an altered adaptive soundtrack file', + mutate: async (directory: string) => writeFile(join(directory, 'assets/nightdrive-drive-lift.mp3'), 'altered'), + error: /digest does not match|hash/u, + }, + { + name: 'an unreferenced adaptive soundtrack file', + mutate: async (directory: string) => writeFile(join(directory, 'assets/nightdrive-unreferenced.opus'), 'unreferenced'), + error: /unexpected artifact|unreferenced/u, + }, + ])('rejects $name', async ({ mutate, error }) => { + const { directory, inputs, contract } = await writeValidArtifacts(); + await mutate(directory); + await expect(verifyArtifactDirectory(directory, inputs, contract)).rejects.toThrow(error); + }); + it.each([ { name: 'an unexpected preview sitemap', diff --git a/tests/performance/budgets.test.ts b/tests/performance/budgets.test.ts index 176c5f9..b0afb81 100644 --- a/tests/performance/budgets.test.ts +++ b/tests/performance/budgets.test.ts @@ -31,6 +31,7 @@ describe('release budgets', () => { }); it('pins every exact section 14 threshold', () => { + expect(PERFORMANCE_BUDGETS.deferredAudio).toBe(16 * 1024 * 1024); expect(PERFORMANCE_BUDGETS).toMatchObject({ initialHtmlCriticalCss: 100 * 1024, initialApplicationJs: 120 * 1024, @@ -46,7 +47,7 @@ describe('release budgets', () => { deferredProjectVisuals: 2.5 * 1024 * 1024, highTierExtras: 750 * 1024, deferredNonVideo: 3.5 * 1024 * 1024, - deferredAudio: 13 * 1024 * 1024, + deferredAudio: 16 * 1024 * 1024, imageSceneConcurrency: 2, videoConcurrency: 1, mobileTextureDimension: 2048, @@ -63,6 +64,10 @@ describe('release budgets', () => { expect(classifyArtifact('assets/nightdrive-grand-tourer-a.glb', '')).toBe('first-live-scene'); expect(classifyArtifact('assets/nightdrive-main.opus', '')).toBe('soundtrack-audio'); expect(classifyArtifact('assets/nightdrive-intro.mp3', '')).toBe('soundtrack-audio'); + expect(classifyArtifact('assets/nightdrive-afterglow.opus', '')).toBe('soundtrack-audio'); + expect(classifyArtifact('assets/nightdrive-afterglow.mp3', '')).toBe('soundtrack-audio'); + expect(classifyArtifact('assets/nightdrive-drive-lift.opus', '')).toBe('soundtrack-audio'); + expect(classifyArtifact('assets/nightdrive-drive-lift.mp3', '')).toBe('soundtrack-audio'); expect(classifyArtifact('assets/high-tier-b.js', 'optional bloom')).toBe('high-tier-extras'); expect(() => classifyArtifact('assets/mystery.bin', '')).toThrow(/unclassified/u); }); diff --git a/tests/unit/content-schemas.test.ts b/tests/unit/content-schemas.test.ts index 7388afe..9b6af50 100644 --- a/tests/unit/content-schemas.test.ts +++ b/tests/unit/content-schemas.test.ts @@ -27,6 +27,8 @@ type MutableContentFixture = { soundtrack?: { intro: { opusAssetId: string; mp3AssetId: string }; main: { opusAssetId: string; mp3AssetId: string }; + afterglow: { opusAssetId: string; mp3AssetId: string }; + driveLift: { opusAssetId: string; mp3AssetId: string }; }; }; @@ -48,6 +50,8 @@ describe('content schemas', () => { content.soundtrack = { intro: { opusAssetId: 'soundtrack-intro-opus', mp3AssetId: 'soundtrack-intro-mp3' }, main: { opusAssetId: 'soundtrack-main-opus', mp3AssetId: 'soundtrack-main-mp3' }, + afterglow: { opusAssetId: 'soundtrack-afterglow-opus', mp3AssetId: 'soundtrack-afterglow-mp3' }, + driveLift: { opusAssetId: 'soundtrack-drive-lift-opus', mp3AssetId: 'soundtrack-drive-lift-mp3' }, }; const audioMetadata = { width: null, @@ -61,6 +65,10 @@ describe('content schemas', () => { { id: 'soundtrack-intro-mp3', fileName: 'nightdrive-intro.mp3', sha256: 'b'.repeat(64), mediaType: 'audio/mpeg', durationMs: 56_064, ...audioMetadata }, { id: 'soundtrack-main-opus', fileName: 'nightdrive-main.opus', sha256: 'c'.repeat(64), mediaType: 'audio/ogg', durationMs: 178_605, ...audioMetadata }, { id: 'soundtrack-main-mp3', fileName: 'nightdrive-main.mp3', sha256: 'd'.repeat(64), mediaType: 'audio/mpeg', durationMs: 178_605, ...audioMetadata }, + { id: 'soundtrack-afterglow-opus', fileName: 'nightdrive-afterglow.opus', sha256: 'e'.repeat(64), mediaType: 'audio/ogg', durationMs: 56_064, ...audioMetadata }, + { id: 'soundtrack-afterglow-mp3', fileName: 'nightdrive-afterglow.mp3', sha256: 'f'.repeat(64), mediaType: 'audio/mpeg', durationMs: 56_064, ...audioMetadata }, + { id: 'soundtrack-drive-lift-opus', fileName: 'nightdrive-drive-lift.opus', sha256: '1'.repeat(64), mediaType: 'audio/ogg', durationMs: 178_605, ...audioMetadata }, + { id: 'soundtrack-drive-lift-mp3', fileName: 'nightdrive-drive-lift.mp3', sha256: '2'.repeat(64), mediaType: 'audio/mpeg', durationMs: 178_605, ...audioMetadata }, ]; const manifest = { assets: [...(validManifest as { assets: unknown[] }).assets, ...soundtrackAssets],