Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f29884db | ||
|
|
ad6b6b2834 | ||
|
|
bbfba94454 | ||
|
|
f2b2e2b79d | ||
|
|
71828c2379 | ||
|
|
e426798c42 |
+9
-5
@@ -1,16 +1,20 @@
|
||||
# Stage 1: Build
|
||||
FROM node:22-alpine AS builder
|
||||
# hugomods image bundles Hugo Extended + Node at matching, tested versions —
|
||||
# Hugo's css.TailwindCSS execs node_modules/.bin/tailwindcss during the build,
|
||||
# and pnpm-workspace.yaml's preferSymlinkedExecutables makes that a plain
|
||||
# symlink Hugo's exec-security check accepts (see layouts/partials/head.html).
|
||||
FROM hugomods/hugo:debian-node-0.165.0 AS builder
|
||||
|
||||
# Install Hugo + FFmpeg (video compression)
|
||||
RUN apk add --no-cache hugo ffmpeg git
|
||||
# Install FFmpeg (video compression)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN npm install -g pnpm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files first (for layer caching)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source files
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "The Empty Throne"
|
||||
description: "The throne room of the dead — obsidian and black iron, the river Styx visible through the far archway, Cerberus at rest beside the dais — is not empty because Hades is absent. It is empty because he is standing directly behind whoever is looking at it."
|
||||
date: 2026-05-28T13:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: true
|
||||
featured: false
|
||||
categories:
|
||||
- Dark
|
||||
- Mythology
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "The Ashen Choir"
|
||||
description: "A masked opera choir stands frozen mid-hymn in a burning gothic theatre, embers drifting through gilded ruin like slow snow as the final note refuses to end."
|
||||
date: 2026-09-13T12:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: false
|
||||
categories:
|
||||
- Dark
|
||||
- Surrealist
|
||||
tags:
|
||||
- Opera
|
||||
- Fire
|
||||
- Masks
|
||||
- Choir
|
||||
- Gothic Theatre
|
||||
- Embers
|
||||
- Macabre
|
||||
slug: the-ashen-choir
|
||||
prompt:
|
||||
scene: "A row of eight robed choir singers stands motionless on the stage of a grand, burning opera house, mouths open mid-note beneath featureless white porcelain masks with delicate gold cracks like veins of gilding. Wide symmetrical shot from the center of the empty stalls, looking directly at the stage. Their robes are charcoal-black velvet trimmed in tarnished gold, hems smoldering at the edges without being consumed. Behind them, the stage curtains burn in slow, silent sheets of orange flame that cast no visible smoke, embers rising instead of falling, drifting upward past the proscenium arch like a reverse snowfall. Sheet music on brass stands in front of each singer curls and blackens at the corners but remains legible. At the center, one singer — taller than the rest — has tilted their masked head back, chin lifted, as if holding an impossibly long final note."
|
||||
environment: "The gilded interior of a 19th-century gothic opera house mid-collapse. Tiered balconies rise on either side, red velvet seats smoldering, gold leaf detailing blistering and curling from the heat. A vast crystal chandelier hangs precariously overhead, half its candles still lit, crystals chiming faintly as the structure shifts. Frescoed ceiling panels depicting weeping muses are scorched at the edges, paint bubbling. Ash and embers drift continuously upward through the auditorium air, catching the footlights' glow. The grand proscenium arch is carved with skeletal cherubs, several of which have cracked and fallen into the orchestra pit below, now empty of musicians."
|
||||
style: "Surrealist gothic theatre horror, painted with the operatic grandeur of a Phantom-of-the-Opera production set ablaze. Rich, saturated palette of deep crimson, black, and molten gold, embers rendered as glowing particulate catching every light source. Reverse-gravity ash and ember physics rendered with total photorealistic conviction despite the impossibility. Hyper-detailed textures: cracked porcelain masks, singed velvet nap, bubbling gold leaf, crystalline chandelier refractions. 8K cinematic render, dramatic underlighting from the footlights, painterly Baroque composition, a stillness that feels suspended mid-collapse rather than static."
|
||||
video: "Embers rise continuously and thickly past the singers in a dense reverse-snowfall, filling the air. The burning curtains behind them ripple and unfurl in constant shifting sheets of flame. The chandelier swings visibly overhead, its crystals chiming and catching the firelight. All eight singers' masked heads tilt further back, shoulders rising and falling visibly as the note builds and holds. Sparks scatter and drift upward continuously from the smoldering seats below. Camera holds static throughout, but everything within the frame moves continuously."
|
||||
---
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "The Clockwork Confessor"
|
||||
description: "A towering brass automaton priest hears confession in a gaslit cathedral, its porcelain saint's mask cracked, gears turning beneath tarnished metal as candlelight gutters through the nave."
|
||||
date: 2026-09-13T10:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: true
|
||||
categories:
|
||||
- Dark
|
||||
- Steampunk
|
||||
tags:
|
||||
- Automaton
|
||||
- Clockwork
|
||||
- Cathedral
|
||||
- Confession
|
||||
- Brass
|
||||
- Gothic
|
||||
- Inquisition
|
||||
slug: the-clockwork-confessor
|
||||
prompt:
|
||||
scene: "A towering brass-and-iron automaton priest kneels within an ornate confessional booth of black oak and wrought iron, half-hidden behind a velvet curtain torn at the hem. Full-figure, three-quarter shot from within the confessional's opposite side, looking through the wooden lattice screen. The automaton's body is a masterwork of exposed clockwork: articulated brass ribs housing spinning escapements, a chest cavity where a slow pendulum swings behind smoked glass, tarnished copper tubing running along both arms venting faint puffs of steam with every mechanical breath. Its hands, folded in a gesture of benediction, are long articulated fingers of blackened steel ending in delicate porcelain fingertips. Its face is a cracked porcelain mask shaped into the serene, downturned features of a saint — one long fissure running from brow to jaw, revealing a single glowing amber optic lens beneath. It holds an ancient leather-bound book, pages fluttering slightly though there is no wind. The mask tilts slowly toward the lattice, listening."
|
||||
environment: "The interior of a vast, decaying Gothic cathedral repurposed as a clockwork sanctuary. Soaring ribbed vaults disappear into darkness overhead, hung with rusted gears the size of cartwheels suspended on chains, turning almost imperceptibly in unison with some unseen mechanism. Rows of empty pews stretch into gloom, dust thick on every surface, candlewax pooled in ancient drifts along the aisle. Stained-glass windows depict saints rendered as machines — halos of gear teeth, wings of articulated brass feathers — filtering fractured amber and blue light across the stone floor. A single overturned prayer kneeler lies near the confessional. Somewhere unseen, a great pendulum clock ticks, its sound echoing through the nave like a heartbeat the building cannot stop."
|
||||
style: "Dark steampunk gothic realism. The visual language of Guillermo del Toro's cathedral interiors fused with Jean-Pierre Jeunet's warm-brass mechanical detailing. Chiaroscuro lighting: deep cathedral shadow pierced by shafts of dusty amber candlelight and cold blue stained-glass spill. Hyper-realistic material rendering — oxidised copper, oil-black iron, cracked antique porcelain with fine craquelure, aged velvet with worn nap. 8K photorealistic render, subtle film grain, anamorphic lens flare where light catches brass edges, painterly gothic composition reminiscent of Baroque religious portraiture."
|
||||
video: "The suspended gears overhead rotate visibly and continuously, their brass teeth catching shifting light. Thick steam billows rhythmically from the automaton's neck vents in visible bursts. The automaton visibly turns its head upward toward the gears, then lowers it back to the book, the crack in its mask glinting as it moves. Its brass fingers turn a page of the open book. Candlelight flickers and dances across every surface, casting moving shadows. Camera holds static throughout, but everything within the frame moves continuously."
|
||||
---
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "The Last Lighthouse"
|
||||
description: "A drowned lighthouse keeper's ghost still tends the lamp atop a crumbling tower, the flooded fishing village below lit by bioluminescent tides that remember every ship it failed to save."
|
||||
date: 2026-09-13T11:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: false
|
||||
categories:
|
||||
- Dark
|
||||
- Horror
|
||||
tags:
|
||||
- Lighthouse
|
||||
- Ghost
|
||||
- Ocean
|
||||
- Drowned Village
|
||||
- Storm
|
||||
- Nautical
|
||||
- Bioluminescence
|
||||
slug: the-last-lighthouse
|
||||
prompt:
|
||||
scene: "A translucent, spectral lighthouse keeper stands within the lamp room at the top of a crumbling stone lighthouse, half-submerged in a storm-flooded sea. Full-figure, wide interior shot looking up through the shattered lamp-room glass toward the figure. The keeper is rendered in wisps of pale blue-white light, oilskin coat and sou'wester hat frayed into trailing luminous vapour at the edges, face obscured beneath the hat's brim save for two faint points of lamplight where eyes should be. One translucent hand rests on the great brass Fresnel lens, its prisms cracked but still turning with a slow, grinding creak, casting a rotating beam of pale gold light out into the storm. Barnacles and dead coral cling to the ironwork railing around him, dripping with seawater that never quite touches his form. He does not react to the storm outside."
|
||||
environment: "A once-thriving fishing village now drowned to its rooftops, visible below through the tower's broken windows — church spire and cottage roofs breaking the black water like tombstones, strings of dead fishing nets tangled in chimney stacks. The sea itself glows faintly bioluminescent blue-green beneath the storm swell, illuminating drowned boats and a single bell tower whose bell still tolls, moved by the current. Lightning forks across a bruised purple-black sky heavy with rain. Gulls wheel and vanish into the dark. The lighthouse tower itself is crumbling, stone slick with salt and moss, iron railings rusted to lace, waves crashing against its base high enough to nearly reach the lamp room."
|
||||
style: "Painterly maritime gothic horror. The atmosphere of The Lighthouse (2019) crossed with bioluminescent deep-sea dread. Cold, desaturated palette dominated by storm-grey and deep indigo, cut through with the warm amber sweep of the lighthouse beam and the eerie cyan glow of the bioluminescent sea. Photorealistic water and glass rendering, volumetric rain and sea-spray, the ghost rendered with soft translucent luminosity that never fully resolves into solid form. 8K cinematic render, heavy atmospheric fog, long lens compression, restrained color grade evoking dread rather than gore."
|
||||
video: "The great Fresnel lens rotates continuously and visibly, its beam sweeping fully around the tower and across the drowned village rooftops below. Rain lashes down in constant, heavy sheets, splattering visibly against the broken glass. Waves crash rhythmically against the tower's base, throwing spray upward. Lightning flashes repeatedly, strobing the storm. The ghostly keeper's translucent form drifts and billows continuously like smoke caught in wind. The distant bell swings and tolls again and again as the current moves it. Camera holds static throughout, but everything within the frame moves continuously."
|
||||
---
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: "The Reliquary Engine"
|
||||
description: "In a Gothic cathedral rebuilt to orbit a dying star, a saint fused with machine drifts among holographic stained glass, her prayer beads cut from data-crystal and her halo a coil of severed fiber-optic light."
|
||||
date: 2026-09-13T14:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: false
|
||||
categories:
|
||||
- Dark
|
||||
- Sci-Fi
|
||||
tags:
|
||||
- Cathedral
|
||||
- Space Station
|
||||
- Android
|
||||
- Holograms
|
||||
- Relics
|
||||
- Zero Gravity
|
||||
- Gothic
|
||||
- Cyberpunk
|
||||
slug: the-reliquary-engine
|
||||
prompt:
|
||||
scene: "A towering android saint floats motionless at the center of a zero-gravity cathedral nave, built into the hull of a derelict orbital station. Full-figure, low wide-angle shot from the aisle looking up at the floating figure. Its body is fused relic and machine: a torso of aged silver-gilt armor plating inlaid with bone fragments behind reinforced glass reliquary windows, one arm ending in an articulated mechanical hand wreathed in soft blue coolant vapor, the other arm entirely gone, replaced by a coiled bundle of glowing fiber-optic cable trailing off into the dark like a severed halo. Its face is a serene carved-ivory mask, cracked along one cheek, a single optical sensor glowing behind the crack where an eye should be. Thick chains of prayer beads made from data-crystal drift around it in the zero gravity, each bead pulsing faintly with light. Below, rows of pews are empty, bolted to curved walls that were once floor."
|
||||
environment: "The interior of a vast Gothic cathedral retrofitted as an orbital station module, ribbed vaults of blackened steel standing in for stone, structural buttresses lined with humming server racks instead of saints' statues. In place of stained glass, towering arched windows are actual plasma displays, casting shifting jewel-toned light — cobalt, violet, deep red — patterned like traditional saints and angels but rendered as scrolling data glyphs and star-charts. Coolant vapor drifts like incense smoke in slow zero-gravity curls. Outside the windows, a dying red star bleeds light across the debris field of a broken fleet. Gargoyle-shaped maintenance drones cling motionless to the vault ribs overhead, sensor-eyes dark. The floor is a mosaic of cracked ceramic tile and exposed circuitry, frost crystallizing along the cold outer wall."
|
||||
style: "Gothic cyberpunk grandeur — the ornate reverence of a medieval cathedral fused with the cold hardware of a derelict space station. Palette of deep void-black and gunmetal grey, cut through with stained-glass jewel tones of cobalt, violet, and crimson from the plasma windows. Hyper-realistic material contrast: aged silver-gilt and carved ivory beside brushed steel, exposed cable, and frost. Volumetric lighting through the plasma windows, coolant vapor lit like incense smoke, zero-gravity debris catching light as it drifts. 8K photorealistic render, anamorphic lens flare from the dying star outside, painterly Gothic composition merged with hard science-fiction detailing."
|
||||
video: "The android saint's mechanical hand drifts slowly upward in the zero gravity, coolant vapor curling visibly off its fingers. Its remaining fiber-optic arm sways and coils gently, cables catching pulses of light that travel visibly along their length. The data-crystal prayer beads rotate and bump against one another, each flaring brighter as they touch. The plasma windows behind it shift continuously, glyphs and star-charts scrolling and reforming. One gargoyle-drone's sensor-eye flickers on briefly, tracking the room, then dims again. Loose coolant vapor drifts thickly across the nave. Camera holds static throughout, but everything within the frame moves continuously."
|
||||
---
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "The Serpent's Bargain"
|
||||
description: "A sand-swallowed desert temple hides an ancient oracle whose lower body coils into a serpent of living dune-glass, offering a bargain no mortal has ever refused twice."
|
||||
date: 2026-09-13T13:00:00+02:00
|
||||
author: "valknar"
|
||||
featured: false
|
||||
categories:
|
||||
- Dark
|
||||
- Mythology
|
||||
tags:
|
||||
- Oracle
|
||||
- Serpent
|
||||
- Desert Temple
|
||||
- Sandstorm
|
||||
- Ancient Ruins
|
||||
- Mythology
|
||||
- Bargain
|
||||
slug: the-serpents-bargain
|
||||
prompt:
|
||||
scene: "An ancient oracle sits enthroned within the ruins of a sand-swallowed desert temple, her upper body regally human — bronzed skin, an elaborate headdress of fused gold and sun-bleached bone, dozens of thin gold bangles stacked along both forearms — while her lower body tapers into the immense coiled form of a serpent made of shifting, glasslike sand, its scales catching light like cut amber glass. Full-figure, low-angle shot emphasizing her height and the serpent coils pooling around a shattered stone altar. One hand extends forward, palm up, offering a small black hourglass in which the sand flows upward instead of down. Her eyes are solid gold, pupil-less, fixed on something just past the viewer. Half her serpent coil disappears into a fissure in the temple floor, from which more sand continuously pours."
|
||||
environment: "The ruined inner sanctum of a temple buried to its rooftop in a vast desert, a shaft of harsh white sunlight cutting down through a collapsed section of ceiling to illuminate the oracle directly. Sandstone columns, cracked and half-swallowed, are carved with reliefs of serpents devouring their own tails. A sandstorm rages just beyond the temple's broken outer wall, visible through gaps as a churning ochre wall of wind, kept entirely at bay within the sanctum as if held by will alone. Loose sand rivers in slow, deliberate streams across the floor toward the oracle's coiled body rather than settling naturally. Broken statuary — headless guardian figures — lines the sanctum's edges, half-buried."
|
||||
style: "Mythic desert surrealism rendered with the scale and reverence of ancient Egyptian and Mesopotamian temple imagery, crossed with an alien-oracle stillness. Palette of scorched gold, deep umber shadow, and pale desert-glare white, with the serpent coil rendered in translucent amber-glass sand that catches and refracts the shaft of sunlight. Hyper-realistic material fidelity: sun-cracked stone, hammered gold jewellery, individually rendered sand grains catching light along the serpent's coils. 8K photorealistic render, volumetric sunbeam with visible dust motes, monumental symmetrical composition."
|
||||
video: "Sand pours continuously and visibly upward out of the inverted hourglass in a steady stream. The oracle's serpent coils shift and slither visibly, scales flexing and catching the light as they resettle. Loose sand streams visibly across the floor toward her in constant rivulets. Beyond the broken wall, the sandstorm churns and billows violently, its edge visibly roiling against the unseen barrier. The oracle's extended hand tilts further forward, offering the hourglass more insistently. Camera holds static throughout, but everything within the frame moves continuously."
|
||||
---
|
||||
@@ -1,5 +1,5 @@
|
||||
baseURL = "https://pivoine.art/"
|
||||
languageCode = "en-us"
|
||||
locale = "en-US"
|
||||
title = "Pivoine"
|
||||
copyright = "© 2026 Pivoine Editorial"
|
||||
|
||||
@@ -79,3 +79,6 @@ noClasses = true
|
||||
|
||||
[build]
|
||||
writeStats = true
|
||||
|
||||
[security.exec]
|
||||
allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^npx$', '^tailwindcss$']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ .Site.LanguageCode }}" class="scroll-smooth">
|
||||
<html lang="{{ .Site.Language.Locale }}" class="scroll-smooth">
|
||||
<head>
|
||||
{{- partial "head.html" . -}}
|
||||
</head>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
/>
|
||||
<meta property="og:url" content="{{ .Permalink }}" />
|
||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||
<meta property="og:locale" content="{{ .Site.LanguageCode | default "en_US" | replaceRE "-" "_" }}" />
|
||||
<meta property="og:locale" content="{{ .Site.Language.Locale | default "en_US" | replaceRE "-" "_" }}" />
|
||||
{{- $ogImage := .Site.Params.ogImage -}}
|
||||
{{- $ogImageAlt := .Site.Title -}}
|
||||
{{- with .Params.banner -}}
|
||||
@@ -112,8 +112,8 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap"
|
||||
/></noscript>
|
||||
|
||||
<!-- CSS via Hugo Pipes + PostCSS + Tailwind v4 -->
|
||||
{{- $css := resources.Get "css/main.css" | css.PostCSS -}}
|
||||
<!-- CSS via Hugo Pipes + Tailwind v4 (native css.TailwindCSS, no PostCSS) -->
|
||||
{{- $css := resources.Get "css/main.css" | css.TailwindCSS -}}
|
||||
{{- if eq hugo.Environment "production" -}}
|
||||
{{- $css = $css | minify | fingerprint "sha256" -}}
|
||||
{{- end -}}
|
||||
|
||||
+1
-4
@@ -9,10 +9,7 @@
|
||||
"format:check": "prettier --check \"layouts/**/*.html\" \"assets/**/*.css\" \"assets/**/*.js\" \"content/**/*.md\" \"*.toml\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.5.0",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-go-template": "^0.0.15",
|
||||
"prettier-plugin-toml": "^2.0.6",
|
||||
|
||||
Generated
+261
-526
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
preferSymlinkedExecutables: true
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Executable
+233
@@ -0,0 +1,233 @@
|
||||
#!/bin/sh
|
||||
# Generate a post's banner video (01.mp4) and preview image (01.png) via
|
||||
# the Replicate black-forest-labs/flux-3 text-to-video model, using the
|
||||
# prompt.{scene,environment,style,video} fields already present in the
|
||||
# post's frontmatter.
|
||||
#
|
||||
# Usage:
|
||||
# sh scripts/create-video.sh <path-to-post-dir-or-index.md> [--force] [--dry-run]
|
||||
#
|
||||
# Requires: REPLICATE_API_TOKEN exported in the environment.
|
||||
#
|
||||
# Video settings (matching every existing post's square/silent convention,
|
||||
# just at higher resolution): 1:1 aspect ratio, 1080p, 5s, no audio.
|
||||
|
||||
set -e
|
||||
|
||||
print_usage() {
|
||||
echo "Usage: sh scripts/create-video.sh <post-dir-or-index.md> [options]"
|
||||
echo ""
|
||||
echo "Generates a post's banner video (01.mp4) via the Replicate flux-3"
|
||||
echo "text-to-video model, using that post's prompt.{scene,environment,style,video}"
|
||||
echo "frontmatter fields, then extracts the first frame as 01.png."
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " <post-dir-or-index.md> Path to content/posts/<slug>/ or its index.md"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -f, --force Overwrite existing 01.mp4 / 01.png"
|
||||
echo " --dry-run Print the constructed prompt and request JSON, make no API call"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Requires REPLICATE_API_TOKEN to be exported in the environment."
|
||||
}
|
||||
|
||||
FORCE=0
|
||||
DRY_RUN=0
|
||||
POST_ARG=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-f|--force) FORCE=1 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-h|--help) print_usage; exit 0 ;;
|
||||
-*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
exit 1
|
||||
;;
|
||||
*) POST_ARG="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$POST_ARG" ]; then
|
||||
print_usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$POST_ARG" ]; then
|
||||
BUNDLE_DIR="${POST_ARG%/}"
|
||||
INDEX_MD="$BUNDLE_DIR/index.md"
|
||||
else
|
||||
INDEX_MD="$POST_ARG"
|
||||
BUNDLE_DIR=$(dirname "$POST_ARG")
|
||||
fi
|
||||
|
||||
if [ ! -f "$INDEX_MD" ]; then
|
||||
echo "Error: $INDEX_MD not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$REPLICATE_API_TOKEN" ]; then
|
||||
echo "Error: REPLICATE_API_TOKEN is not set in your environment." >&2
|
||||
echo "Export it before running this script, e.g.:" >&2
|
||||
echo " export REPLICATE_API_TOKEN=r8_..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST_MP4="$BUNDLE_DIR/01.mp4"
|
||||
DEST_PNG="$BUNDLE_DIR/01.png"
|
||||
|
||||
if [ "$FORCE" -ne 1 ]; then
|
||||
if [ -e "$DEST_MP4" ]; then
|
||||
echo "Error: $DEST_MP4 already exists (use --force to overwrite)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e "$DEST_PNG" ]; then
|
||||
echo "Error: $DEST_PNG already exists (use --force to overwrite)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
PROMPT_JSON=$(yq --front-matter=extract -o=json '.prompt' "$INDEX_MD")
|
||||
|
||||
if [ "$PROMPT_JSON" = "null" ] || [ -z "$PROMPT_JSON" ]; then
|
||||
echo "Error: $INDEX_MD has no prompt.* fields in its frontmatter" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCENE=$(printf '%s' "$PROMPT_JSON" | jq -r '.scene // empty')
|
||||
ENVIRONMENT=$(printf '%s' "$PROMPT_JSON" | jq -r '.environment // empty')
|
||||
STYLE=$(printf '%s' "$PROMPT_JSON" | jq -r '.style // empty')
|
||||
VIDEO=$(printf '%s' "$PROMPT_JSON" | jq -r '.video // empty')
|
||||
|
||||
if [ -z "$SCENE" ]; then
|
||||
echo "Error: prompt.scene is missing/empty in $INDEX_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$ENVIRONMENT" ]; then
|
||||
echo "Error: prompt.environment is missing/empty in $INDEX_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$STYLE" ]; then
|
||||
echo "Error: prompt.style is missing/empty in $INDEX_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$VIDEO" ]; then
|
||||
echo "Error: prompt.video is missing/empty in $INDEX_MD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLUG=$(yq --front-matter=extract '.slug' "$INDEX_MD")
|
||||
echo "▶ ${SLUG:-$(basename "$BUNDLE_DIR")}: building prompt from frontmatter"
|
||||
|
||||
PROMPT="${SCENE} ${ENVIRONMENT} ${STYLE} ${VIDEO}"
|
||||
|
||||
INPUT_JSON=$(jq -n \
|
||||
--arg prompt "$PROMPT" \
|
||||
--arg duration "5" \
|
||||
--arg resolution "1080p" \
|
||||
--arg aspect_ratio "1:1" \
|
||||
'{
|
||||
input: {
|
||||
prompt: $prompt,
|
||||
images: [],
|
||||
duration: $duration,
|
||||
resolution: $resolution,
|
||||
aspect_ratio: $aspect_ratio,
|
||||
generate_audio: false
|
||||
}
|
||||
}')
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "--- Constructed prompt ---"
|
||||
echo "$PROMPT"
|
||||
echo "--- Request body ---"
|
||||
echo "$INPUT_JSON" | jq .
|
||||
echo "(dry run — no API call made)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "▶ Submitting generation request to Replicate (flux-3)..."
|
||||
CREATE_RESPONSE=$(curl -sS \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $REPLICATE_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: wait=60" \
|
||||
-d "$INPUT_JSON" \
|
||||
https://api.replicate.com/v1/models/black-forest-labs/flux-3/predictions \
|
||||
-w '\n%{http_code}')
|
||||
|
||||
HTTP_CODE=$(printf '%s' "$CREATE_RESPONSE" | tail -n1)
|
||||
BODY=$(printf '%s' "$CREATE_RESPONSE" | sed '$d')
|
||||
|
||||
case "$HTTP_CODE" in
|
||||
2??) : ;;
|
||||
*)
|
||||
echo "Error: Replicate API returned HTTP $HTTP_CODE" >&2
|
||||
echo "$BODY" | jq -r '.detail // .error // .' >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
STATUS=$(printf '%s' "$BODY" | jq -r '.status')
|
||||
GET_URL=$(printf '%s' "$BODY" | jq -r '.urls.get')
|
||||
PREDICTION_ID=$(printf '%s' "$BODY" | jq -r '.id')
|
||||
POLL_BODY="$BODY"
|
||||
|
||||
if [ -z "$GET_URL" ] || [ "$GET_URL" = "null" ]; then
|
||||
echo "Error: unexpected response from Replicate (no urls.get):" >&2
|
||||
echo "$BODY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLL_INTERVAL=5
|
||||
MAX_WAIT=600
|
||||
ELAPSED=0
|
||||
|
||||
while :; do
|
||||
case "$STATUS" in
|
||||
succeeded) break ;;
|
||||
failed|canceled)
|
||||
ERR=$(printf '%s' "$POLL_BODY" | jq -r '.error // "unknown error"')
|
||||
echo "Error: prediction $STATUS: $ERR" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "Error: timed out after ${MAX_WAIT}s waiting on prediction $PREDICTION_ID" >&2
|
||||
echo "Check status manually: curl -H \"Authorization: Bearer \$REPLICATE_API_TOKEN\" $GET_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
||||
|
||||
POLL_BODY=$(curl -sS -H "Authorization: Bearer $REPLICATE_API_TOKEN" "$GET_URL")
|
||||
STATUS=$(printf '%s' "$POLL_BODY" | jq -r '.status')
|
||||
echo " … ${ELAPSED}s elapsed, status: $STATUS"
|
||||
done
|
||||
|
||||
OUTPUT_URL=$(printf '%s' "$POLL_BODY" | jq -r '.output')
|
||||
if [ -z "$OUTPUT_URL" ] || [ "$OUTPUT_URL" = "null" ]; then
|
||||
echo "Error: prediction succeeded but no output URL was returned" >&2
|
||||
echo "$POLL_BODY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/create-video.XXXXXX")
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
TMP_MP4="$TMP_DIR/01.mp4"
|
||||
TMP_PNG="$TMP_DIR/01.png"
|
||||
|
||||
echo "▶ Downloading generated video..."
|
||||
curl -fSL -o "$TMP_MP4" "$OUTPUT_URL"
|
||||
|
||||
echo "▶ Extracting first frame as preview PNG..."
|
||||
ffmpeg -y -loglevel error -i "$TMP_MP4" -vframes 1 "$TMP_PNG" < /dev/null
|
||||
|
||||
mv "$TMP_MP4" "$DEST_MP4"
|
||||
mv "$TMP_PNG" "$DEST_PNG"
|
||||
|
||||
echo "✓ Wrote $DEST_MP4 and $DEST_PNG"
|
||||
Reference in New Issue
Block a user