assets: add approved portfolio evidence media
@@ -1,49 +1,13 @@
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"id": "soundtrack-intro-opus",
|
||||
"fileName": "nightdrive-intro.opus",
|
||||
"sha256": "561c05cea71d0920ec323375644039007b91b0a4c346dc663e233ad42d6ab490",
|
||||
"mediaType": "audio/ogg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 56064,
|
||||
"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-intro-mp3",
|
||||
"fileName": "nightdrive-intro.mp3",
|
||||
"sha256": "008794ebd7f545c32f19aab4ada6aba40d9fc3341830a66e77f21d87a41c009b",
|
||||
"id": "soundtrack-afterglow-mp3",
|
||||
"fileName": "nightdrive-afterglow.mp3",
|
||||
"sha256": "685687ad131d61c34e42bcee29af16a4c8180d788c67d75f2331fa9082a3a44e",
|
||||
"mediaType": "audio/mpeg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 56064,
|
||||
"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-main-opus",
|
||||
"fileName": "nightdrive-main.opus",
|
||||
"sha256": "81933fc8e589428ef07fb265572a79975f183848b6a23616cb289e33fe3fb88c",
|
||||
"mediaType": "audio/ogg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 178605,
|
||||
"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-main-mp3",
|
||||
"fileName": "nightdrive-main.mp3",
|
||||
"sha256": "796de8247c4cb8012d7c6883b534112ac09a40255a2c33cd68acf0f87c066659",
|
||||
"mediaType": "audio/mpeg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 178605,
|
||||
"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"
|
||||
@@ -61,13 +25,13 @@
|
||||
"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",
|
||||
"id": "soundtrack-drive-lift-mp3",
|
||||
"fileName": "nightdrive-drive-lift.mp3",
|
||||
"sha256": "edcb802938d3d1e53392f9ccd2e153f72a24b494c205ce6f556d73de5fe709ec",
|
||||
"mediaType": "audio/mpeg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 25116,
|
||||
"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"
|
||||
@@ -85,16 +49,136 @@
|
||||
"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",
|
||||
"id": "soundtrack-intro-mp3",
|
||||
"fileName": "nightdrive-intro.mp3",
|
||||
"sha256": "008794ebd7f545c32f19aab4ada6aba40d9fc3341830a66e77f21d87a41c009b",
|
||||
"mediaType": "audio/mpeg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 11163,
|
||||
"durationMs": 56064,
|
||||
"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-intro-opus",
|
||||
"fileName": "nightdrive-intro.opus",
|
||||
"sha256": "561c05cea71d0920ec323375644039007b91b0a4c346dc663e233ad42d6ab490",
|
||||
"mediaType": "audio/ogg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 56064,
|
||||
"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-main-mp3",
|
||||
"fileName": "nightdrive-main.mp3",
|
||||
"sha256": "796de8247c4cb8012d7c6883b534112ac09a40255a2c33cd68acf0f87c066659",
|
||||
"mediaType": "audio/mpeg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 178605,
|
||||
"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-main-opus",
|
||||
"fileName": "nightdrive-main.opus",
|
||||
"sha256": "81933fc8e589428ef07fb265572a79975f183848b6a23616cb289e33fe3fb88c",
|
||||
"mediaType": "audio/ogg",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"durationMs": 178605,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Original AI-assisted soundtrack produced by Luca Pîslaru with ACE-Step 1.5"
|
||||
},
|
||||
{
|
||||
"id": "currency-converter-interface",
|
||||
"fileName": "project-currency-converter.webp",
|
||||
"sha256": "57159ae72a075ba5317217233c3c0eb210a485c51b5f2c37b5ed156d5ea37efb",
|
||||
"mediaType": "image/webp",
|
||||
"width": 1440,
|
||||
"height": 820,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "medication-mobile-data-settings",
|
||||
"fileName": "project-medication-data-settings.webp",
|
||||
"sha256": "d951dd723047b737ab6a7da46b82f3f3f3aca7ec8853314f74ac57e1950ce1ba",
|
||||
"mediaType": "image/webp",
|
||||
"width": 720,
|
||||
"height": 1280,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "medication-mobile-logs",
|
||||
"fileName": "project-medication-logs.webp",
|
||||
"sha256": "d34b2d41855e6dca4762fb9fc7728b64a94fef60de36149e465ce50821bc1dc7",
|
||||
"mediaType": "image/webp",
|
||||
"width": 720,
|
||||
"height": 1280,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "medication-mobile-notification-settings",
|
||||
"fileName": "project-medication-notification-settings.webp",
|
||||
"sha256": "d2fe0076d6581e76c361cb656350c58f6685f6fe91c5b6c5871e60977e8d5be7",
|
||||
"mediaType": "image/webp",
|
||||
"width": 720,
|
||||
"height": 1280,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "opinion-final-distribution-chart",
|
||||
"fileName": "project-opinion-final-distribution.webp",
|
||||
"sha256": "2f2f6d64b8bb4f79ecbce1a8bc486cf7bbdf4087eb4f10062a6fe889246dbdf1",
|
||||
"mediaType": "image/webp",
|
||||
"width": 571,
|
||||
"height": 455,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "opinion-polarization-chart",
|
||||
"fileName": "project-opinion-polarization.webp",
|
||||
"sha256": "77f0d5076769e6e45b566c5f322d8f535686f9e9f928e18fe4c16b3dac989c43",
|
||||
"mediaType": "image/webp",
|
||||
"width": 584,
|
||||
"height": 455,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
},
|
||||
{
|
||||
"id": "social-preview-nightdrive",
|
||||
"fileName": "project-social-preview.webp",
|
||||
"sha256": "ed470760643ef3d413b6ca4a0e1434771fcc9b61b6ac34011feae77a13f2daca",
|
||||
"mediaType": "image/webp",
|
||||
"width": 1200,
|
||||
"height": 630,
|
||||
"durationMs": null,
|
||||
"rightsHolder": "Luca Pîslaru",
|
||||
"license": "All rights reserved",
|
||||
"attribution": "Luca Pîslaru"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,13 +4,9 @@
|
||||
{ "id": "medication-mobile-logs", "kind": "image", "output": "project-medication-logs.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "medication-mobile-data-settings", "kind": "image", "output": "project-medication-data-settings.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "medication-mobile-notification-settings", "kind": "image", "output": "project-medication-notification-settings.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "footy-detection-frame", "kind": "image", "output": "project-footy-detection.webp", "maxWidth": 1600, "rightsHolder": "Third-party footage and Luca Pîslaru", "license": "Publication requires confirmation", "attribution": "Detection overlay by Luca Pîslaru" },
|
||||
{ "id": "opinion-polarization-chart", "kind": "image", "output": "project-opinion-polarization.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "opinion-cascade-thresholds-chart", "kind": "image", "output": "project-opinion-cascade-thresholds.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "opinion-average-degree-chart", "kind": "image", "output": "project-opinion-average-degree.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "opinion-revolt-frequency-chart", "kind": "image", "output": "project-opinion-revolt-frequency.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "opinion-final-distribution-chart", "kind": "image", "output": "project-opinion-final-distribution.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "private-game-loop", "kind": "video", "output": "project-private-game-loop.webm", "maxWidth": 1280, "startSeconds": 3, "durationSeconds": 5, "rightsHolder": "Rights holder confirmation pending", "license": "Publication requires confirmation", "attribution": "Prototype contribution by Luca Pîslaru" },
|
||||
{ "id": "currency-converter-interface", "kind": "image", "output": "project-currency-converter.webp", "maxWidth": 1440, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" }
|
||||
{ "id": "currency-converter-interface", "kind": "image", "output": "project-currency-converter.webp", "maxWidth": 1440, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
|
||||
{ "id": "social-preview-nightdrive", "kind": "image", "output": "project-social-preview.webp", "maxWidth": 1200, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" }
|
||||
]
|
||||
}
|
||||
|
||||
BIN
inputs/prepared-assets/project-currency-converter.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
inputs/prepared-assets/project-medication-data-settings.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
inputs/prepared-assets/project-medication-logs.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 31 KiB |
BIN
inputs/prepared-assets/project-opinion-final-distribution.webp
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
inputs/prepared-assets/project-opinion-polarization.webp
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
inputs/prepared-assets/project-social-preview.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
74
scripts/generate-social-preview.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdir, realpath } from 'node:fs/promises';
|
||||
import { dirname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const permittedPrivateRoot = resolve(repositoryRoot, 'content/private');
|
||||
|
||||
const fail = () => {
|
||||
throw new Error('SOCIAL-PREVIEW-PATH-001');
|
||||
};
|
||||
|
||||
const canonicalizeForWrite = async (path) => {
|
||||
const absolute = resolve(path);
|
||||
let cursor = dirname(absolute);
|
||||
const suffix = [];
|
||||
while (true) {
|
||||
try {
|
||||
return resolve(await realpath(cursor), ...suffix, absolute.slice(dirname(absolute).length + 1));
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) fail();
|
||||
const parent = dirname(cursor);
|
||||
if (parent === cursor) fail();
|
||||
suffix.unshift(cursor.slice(parent.length + 1));
|
||||
cursor = parent;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isInside = (path, root) => path === root || path.startsWith(`${root}${sep}`);
|
||||
|
||||
export const generateSocialPreview = async (outputPath) => {
|
||||
if (typeof outputPath !== 'string' || outputPath.trim() === '') fail();
|
||||
const output = await canonicalizeForWrite(outputPath);
|
||||
const privateRoot = await canonicalizeForWrite(permittedPrivateRoot);
|
||||
if (!isInside(output, privateRoot) && !isInside(output, '/tmp')) fail();
|
||||
|
||||
const svg = Buffer.from(`
|
||||
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1200" height="630" fill="#070910"/>
|
||||
<rect y="356" width="1200" height="274" fill="#0b1019"/>
|
||||
<path d="M0 356H1200" stroke="#1b3246" stroke-width="2"/>
|
||||
<circle cx="880" cy="174" r="78" fill="#152638"/>
|
||||
<path d="M474 630L586 356H642L780 630Z" fill="#101b28"/>
|
||||
<path d="M544 630L606 356" stroke="#35c8d0" stroke-width="7" opacity="0.9"/>
|
||||
<path d="M701 630L623 356" stroke="#e3a44a" stroke-width="7" opacity="0.88"/>
|
||||
<path d="M602 566L613 522M614 490L619 458M620 430L623 405" stroke="#dfe9ef" stroke-width="5" stroke-linecap="round" opacity="0.72"/>
|
||||
<text x="92" y="183" font-family="DejaVu Sans, sans-serif" font-size="66" font-weight="700" fill="#f1f5f7">Luca Pîslaru</text>
|
||||
<text x="96" y="245" font-family="DejaVu Sans, sans-serif" font-size="29" font-weight="500" letter-spacing="0.6" fill="#9fc8d1">System Administrator & Full-Stack Developer</text>
|
||||
<path d="M96 286H342" stroke="#35c8d0" stroke-width="4"/>
|
||||
<path d="M350 286H430" stroke="#e3a44a" stroke-width="4"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await sharp(svg, { density: 72 })
|
||||
.flatten({ background: '#070910' })
|
||||
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
||||
.toFile(output);
|
||||
return output;
|
||||
};
|
||||
|
||||
const parseOutput = (argv) => {
|
||||
if (argv.length !== 2 || argv[0] !== '--output') fail();
|
||||
return argv[1];
|
||||
};
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
generateSocialPreview(parseOutput(process.argv.slice(2))).catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : 'SOCIAL-PREVIEW-001'}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import { scanImage, scanText } from '../../scripts/lib/privacy-scan.mjs';
|
||||
import { mergeAssetRecords } from '../../scripts/lib/merge-assets.mjs';
|
||||
// @ts-expect-error The production script is native ESM and intentionally has no TypeScript runtime wrapper.
|
||||
import { prepareAssets, publishPreparedAssets, verifyPreparedAssets } from '../../scripts/prepare-assets.mjs';
|
||||
// @ts-expect-error The production script is native ESM and intentionally has no TypeScript runtime wrapper.
|
||||
import { generateSocialPreview } from '../../scripts/generate-social-preview.mjs';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
interface Candidate { id: string; fileName: string; sha256: string; }
|
||||
@@ -293,6 +295,31 @@ describe('prepared candidate publication', () => {
|
||||
});
|
||||
|
||||
describe('deterministic asset preparation', () => {
|
||||
it('renders the deterministic social preview at 1200×630 as opaque privacy-safe pixels', async () => {
|
||||
const root = await createRoot();
|
||||
const firstPath = join(root, 'social-preview-one.png');
|
||||
const secondPath = join(root, 'social-preview-two.png');
|
||||
await generateSocialPreview(firstPath);
|
||||
await generateSocialPreview(secondPath);
|
||||
|
||||
const metadata = await sharp(firstPath).metadata();
|
||||
const stats = await sharp(firstPath).stats();
|
||||
expect(metadata.width).toBe(1200);
|
||||
expect(metadata.height).toBe(630);
|
||||
expect(stats.isOpaque).toBe(true);
|
||||
expect(await hashFile(secondPath)).toBe(await hashFile(firstPath));
|
||||
await expect(scanImage(firstPath, [
|
||||
'private canary identity',
|
||||
'private canary domain',
|
||||
'private canary medication',
|
||||
], { runOcr: true })).resolves.toMatchObject({ width: 1200, height: 630 });
|
||||
}, 30_000);
|
||||
|
||||
it('rejects social-preview output paths outside private or temporary storage', async () => {
|
||||
await expect(generateSocialPreview(join(process.cwd(), 'public', 'unsafe.png')))
|
||||
.rejects.toThrow(/SOCIAL-PREVIEW-PATH-001/u);
|
||||
});
|
||||
|
||||
it('re-encodes images without metadata under generated safe filenames, identically across runs', async () => {
|
||||
const root = await createRoot();
|
||||
const inputs = await makeInputs(root, baseRecipe());
|
||||
|
||||