diff --git a/src/content/loadTemplate.ts b/src/content/loadTemplate.ts index c01bc3b..53890d1 100644 --- a/src/content/loadTemplate.ts +++ b/src/content/loadTemplate.ts @@ -10,6 +10,10 @@ export interface TemplateData { imagePrompt: string; /** Whether this template uses the artist's face-guide for identity. Defaults to true. */ usesFaceGuide: boolean; + /** Overrides the default face-swap / no-face instruction when set. */ + customInstruction: string | null; + /** Overrides the default person-centric motion prompt when set. */ + customMotionPrompt: string | null; styleGuide: Record | null; captionGuide: Record | null; captionExamples: string[]; @@ -61,11 +65,15 @@ export async function loadTemplate(templateName: string): Promise const imagePrompt = (sg?.imagePrompt as string) ?? ""; // Default to true — most templates use the artist's face const usesFaceGuide = (sg?.usesFaceGuide as boolean) ?? true; + const customInstruction = (sg?.customInstruction as string) ?? null; + const customMotionPrompt = (sg?.customMotionPrompt as string) ?? null; return { name: templateName, imagePrompt, usesFaceGuide, + customInstruction, + customMotionPrompt, styleGuide, captionGuide, captionExamples, @@ -129,6 +137,12 @@ export function buildMotionPrompt(template: TemplateData): string { ? template.videoMoods[Math.floor(Math.random() * template.videoMoods.length)] : ""; + if (template.customMotionPrompt) { + return template.customMotionPrompt + .replace("{movement}", movement) + .replace("{mood}", mood); + } + return `Completely static camera. The person stares at the camera. Movement: ${movement}.${mood ? ` Energy: ${mood}.` : ""} Shot on phone, low light, grainy.`; } diff --git a/src/content/templates/album-record-store/caption-guide.json b/src/content/templates/album-record-store/caption-guide.json new file mode 100644 index 0000000..2d68225 --- /dev/null +++ b/src/content/templates/album-record-store/caption-guide.json @@ -0,0 +1,30 @@ +{ + "templateStyle": "album art on vinyl in a record store — the kind of post an artist makes when their music hits wax for the first time", + "captionRole": "the caption should feel like the artist posted this themselves. proud but not corny. announcing the vinyl, reflecting on the music, or saying something raw about what the album means.", + "tone": "understated pride, like posting a photo of your album in a store and letting the moment speak for itself. not hype-man energy — quiet flex.", + "rules": [ + "lowercase only", + "keep it under 80 characters for short, can go longer for medium/long", + "no punctuation at the end unless its a question mark", + "never sound like a press release or marketing copy", + "never say 'out now' or 'stream now' or 'link in bio'", + "dont describe whats in the image", + "can reference the album, the songs, or what they mean to you", + "can reference the physical vinyl / record store experience", + "if it sounds like a label wrote it, rewrite it until it sounds like the artist texted it to a friend" + ], + "formats": [ + "a one-line reflection on the album ('i left everything in this one')", + "a quiet flex about being on vinyl ('never thought id see this in a store')", + "a nostalgic moment ('used to dig through bins like this looking for something that felt like home')", + "something the listener would screenshot ('this album is the version of me i was scared to show you')", + "a short dedication or thank you that feels real, not performative" + ], + "examples_of_good_length": [ + "i left everything in this one", + "found myself in the crates today", + "this album is the version of me i was scared to show you", + "never thought id see my name on a spine in a record store", + "wrote this in my bedroom now its on wax" + ] +} diff --git a/src/content/templates/album-record-store/references/captions/examples.json b/src/content/templates/album-record-store/references/captions/examples.json new file mode 100644 index 0000000..5d5d383 --- /dev/null +++ b/src/content/templates/album-record-store/references/captions/examples.json @@ -0,0 +1,10 @@ +[ + "i left everything in this one", + "found myself in the crates today", + "never thought id see my name on a spine in a record store", + "wrote this in my bedroom now its on wax", + "this album is the version of me i was scared to show you", + "every scratch on this vinyl is a memory", + "the songs sound different on wax. heavier somehow", + "somebody in new york is gonna find this in a bin one day and feel something" +] diff --git a/src/content/templates/album-record-store/style-guide.json b/src/content/templates/album-record-store/style-guide.json new file mode 100644 index 0000000..3d6eacb --- /dev/null +++ b/src/content/templates/album-record-store/style-guide.json @@ -0,0 +1,36 @@ +{ + "name": "album-record-store", + "description": "Album cover art displayed in a gritty New York record store — vinyl spinning on a turntable", + "usesFaceGuide": true, + "customInstruction": "Place the album cover art from the first image onto a vinyl record sleeve and display it prominently in the scene. The album art should be clearly visible — propped up on a shelf, leaning against a crate, or displayed on the counter next to the turntable. A vinyl record from the same album is spinning on a turntable nearby. Do NOT alter the album art — reproduce it exactly. Remove any text, captions, watermarks, or overlays from the generated scene. The album art itself should retain its original text/design.", + "customMotionPrompt": "Completely static camera. The vinyl record spins slowly on the turntable. {movement} Warm dust-in-the-air feeling. {mood} Shot on phone, warm tungsten lighting.", + "imagePrompt": "A vinyl record spinning on a turntable inside a cramped, rundown New York City record store. The album cover art is displayed next to the turntable, propped against a stack of records. Wooden crate bins full of vinyl records fill the background. Warm tungsten overhead light, dust particles visible in the air. The store feels lived-in — peeling stickers on the counter, handwritten price tags, faded band posters on the walls. Phone camera, slightly warm color cast.", + + "camera": { + "type": "iPhone resting on the counter, recording a quick story", + "angle": "slightly above the turntable, looking down at an angle — like someone held their phone over the record to film it spinning", + "quality": "iPhone video quality — warm color cast from the overhead light, slight lens flare, not perfectly sharp, natural vignetting at corners", + "focus": "turntable and album art in focus, background bins and shelves slightly soft" + }, + + "environment": { + "feel": "a real independent record store in lower Manhattan or Brooklyn — cramped, cluttered, full of character", + "lighting": "warm tungsten bulbs overhead, maybe a small desk lamp near the register. Pools of warm light, deep shadows between the bins. Dust particles catching the light.", + "backgrounds": "wooden crate bins overflowing with vinyl, hand-lettered genre dividers, faded concert posters and stickers on every surface, a boombox or old speakers on a high shelf, maybe a cat sleeping on a stack of records", + "avoid": "clean modern stores, bright fluorescent lighting, empty shelves, corporate branding, pristine surfaces, anything that looks new or staged" + }, + + "subject": { + "expression": "N/A — no person in the shot, the subject is the album and turntable", + "pose": "N/A", + "clothing": "N/A", + "framing": "turntable takes up the lower half of frame, album art visible in the upper portion or to the side, surrounded by the store environment" + }, + + "realism": { + "priority": "this MUST look like a real phone video taken inside an actual NYC record store, not a render or AI image", + "texture": "warm grain from the phone camera, slight dust and scratches visible on the vinyl, wood grain on the crate bins, worn edges on the record sleeves", + "imperfections": "fingerprints on the vinyl, slightly crooked album display, a price sticker on the sleeve, dust on the turntable platter, uneven stacks of records in the background", + "avoid": "clean renders, perfect symmetry, bright even lighting, glossy surfaces, anything that looks digital or AI-generated, stock-photo record stores" + } +} diff --git a/src/content/templates/album-record-store/video-moods.json b/src/content/templates/album-record-store/video-moods.json new file mode 100644 index 0000000..90d7c99 --- /dev/null +++ b/src/content/templates/album-record-store/video-moods.json @@ -0,0 +1,10 @@ +[ + "warm nostalgia, like walking into a place that reminds you of being a kid", + "quiet pride, the feeling of seeing something you made exist in the real world", + "intimate, like youre showing a close friend something that matters to you", + "reverent, the way people handle vinyl carefully because it feels sacred", + "bittersweet, like the album captured a version of you that doesnt exist anymore", + "hypnotic, the kind of calm that comes from watching something spin in circles", + "peaceful solitude, alone in the store after hours", + "wistful, like remembering the sessions that made this album" +] diff --git a/src/content/templates/album-record-store/video-movements.json b/src/content/templates/album-record-store/video-movements.json new file mode 100644 index 0000000..efa9aa9 --- /dev/null +++ b/src/content/templates/album-record-store/video-movements.json @@ -0,0 +1,10 @@ +[ + "the vinyl spins steadily, tonearm tracking the groove, dust particles drift through the warm light", + "camera slowly drifts closer to the album art, the vinyl keeps spinning in the background", + "a hand reaches into frame and gently places the needle on the record", + "the turntable spins, the overhead light flickers once, dust motes float lazily", + "someone flips through records in a crate in the background, out of focus, while the vinyl spins", + "the camera barely moves, just the vinyl spinning and the warm light shifting slightly", + "a slight camera drift to reveal more of the store — bins, posters, clutter — then settles back on the turntable", + "the tonearm rides the groove, a tiny reflection of light glints off the spinning vinyl surface" +] diff --git a/src/sandboxes/__tests__/getSandboxEnv.test.ts b/src/sandboxes/__tests__/getSandboxEnv.test.ts index 1942b29..df32977 100644 --- a/src/sandboxes/__tests__/getSandboxEnv.test.ts +++ b/src/sandboxes/__tests__/getSandboxEnv.test.ts @@ -35,6 +35,22 @@ describe("getSandboxEnv", () => { expect(env).not.toHaveProperty("GITHUB_TOKEN"); }); + it("includes CHARTMETRIC_REFRESH_TOKEN when set", () => { + process.env.CHARTMETRIC_REFRESH_TOKEN = "test-cm-token"; + + const env = getSandboxEnv("acc_123"); + + expect(env.CHARTMETRIC_REFRESH_TOKEN).toBe("test-cm-token"); + }); + + it("omits CHARTMETRIC_REFRESH_TOKEN when not set", () => { + delete process.env.CHARTMETRIC_REFRESH_TOKEN; + + const env = getSandboxEnv("acc_123"); + + expect(env).not.toHaveProperty("CHARTMETRIC_REFRESH_TOKEN"); + }); + it("throws when RECOUP_API_KEY is missing", () => { delete process.env.RECOUP_API_KEY; diff --git a/src/sandboxes/getSandboxEnv.ts b/src/sandboxes/getSandboxEnv.ts index 3906254..5e6b14e 100644 --- a/src/sandboxes/getSandboxEnv.ts +++ b/src/sandboxes/getSandboxEnv.ts @@ -20,5 +20,10 @@ export function getSandboxEnv( env.GITHUB_TOKEN = githubToken; } + const chartmetricToken = process.env.CHARTMETRIC_REFRESH_TOKEN; + if (chartmetricToken) { + env.CHARTMETRIC_REFRESH_TOKEN = chartmetricToken; + } + return env; } diff --git a/src/tasks/createContentTask.ts b/src/tasks/createContentTask.ts index 5237ab4..bc8e865 100644 --- a/src/tasks/createContentTask.ts +++ b/src/tasks/createContentTask.ts @@ -109,8 +109,8 @@ export const createContentTask = schemaTask({ // --- Step 5: Generate image --- logStep("Generating image"); const referenceImagePath = pickRandomReferenceImage(template); - // Build prompt: face-swap instruction (if needed) + template scene + style guide - const instruction = template.usesFaceGuide ? FACE_SWAP_INSTRUCTION : NO_FACE_INSTRUCTION; + const instruction = template.customInstruction + ?? (template.usesFaceGuide ? FACE_SWAP_INSTRUCTION : NO_FACE_INSTRUCTION); const basePrompt = `${instruction} ${template.imagePrompt}`; const fullPrompt = buildImagePrompt(basePrompt, template.styleGuide); let imageUrl = await generateContentImage({