Compare commits
20
Commits
84bc1e8d82
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e3ea6719 | ||
|
|
d3c4a06c82 | ||
|
|
63f29884db | ||
|
|
ad6b6b2834 | ||
|
|
bbfba94454 | ||
|
|
f2b2e2b79d | ||
|
|
71828c2379 | ||
|
|
e426798c42 | ||
|
|
a379b0d00f | ||
|
|
1aceeeae36 | ||
|
|
d129c545dd | ||
|
|
420c6466ca | ||
|
|
cf469081f8 | ||
|
|
8e6d8c52a2 | ||
|
|
194157c94c | ||
|
|
3d644c6da0 | ||
|
|
bfe97b9595 | ||
|
|
c8f9c10773 | ||
|
|
987c715c00 | ||
|
|
bc09fa2ad4 |
+9
-5
@@ -1,16 +1,20 @@
|
|||||||
# Stage 1: Build
|
# 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)
|
# Install FFmpeg (video compression)
|
||||||
RUN apk add --no-cache hugo ffmpeg git
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install pnpm
|
# Install pnpm
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files first (for layer caching)
|
# 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
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Copy source files
|
# Copy source files
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
@source "../../content/**/*.md";
|
@source "../../content/**/*.md";
|
||||||
@source "../../assets/js/**/*.js";
|
@source "../../assets/js/**/*.js";
|
||||||
|
|
||||||
|
@custom-variant hover (&:hover);
|
||||||
|
|
||||||
/* ── Design tokens — sneaker drop culture ────────────────────── */
|
/* ── Design tokens — sneaker drop culture ────────────────────── */
|
||||||
@theme {
|
@theme {
|
||||||
/* Dark base — deep navy-black */
|
/* Dark base — deep navy-black */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "The Pale Pour"
|
title: "The Pale Pour"
|
||||||
description: "A skeletal Grim Reaper tends bar in a subterranean Gothic absinthe den, where the Green Fairy's bioluminescent fog coils between candlelit bottles and tarnished mirrors."
|
description: "A skeletal Grim Reaper tends bar in a subterranean Gothic absinthe den, where the Green Fairy's bioluminescent fog coils between candlelit bottles and tarnished mirrors."
|
||||||
date: 2026-05-07
|
date: 2026-05-07T18:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: true
|
featured: true
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "The Count's Chamber"
|
title: "The Count's Chamber"
|
||||||
description: "The room speaks before its master does — an ancient Gothic chamber of firelight and shadow, an armchair that still holds the warmth of centuries, and a wine glass of blood on a small table, patiently waiting."
|
description: "The room speaks before its master does — an ancient Gothic chamber of firelight and shadow, an armchair that still holds the warmth of centuries, and a wine glass of blood on a small table, patiently waiting."
|
||||||
date: 2026-05-08
|
date: 2026-05-08T12:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: false
|
featured: false
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "The Last Valkyrie"
|
title: "The Last Valkyrie"
|
||||||
description: "She walks the field when the battle is already over — magnificent, heavily armed, towering above the fallen — choosing among the dead with the cold authority of a goddess who has never known defeat."
|
description: "She walks the field when the battle is already over — magnificent, heavily armed, towering above the fallen — choosing among the dead with the cold authority of a goddess who has never known defeat."
|
||||||
date: 2026-05-08
|
date: 2026-05-08T14:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: false
|
featured: false
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "The Warlord's Hour"
|
title: "The Warlord's Hour"
|
||||||
description: "A fearsome samurai warlord, clad in magnificent lacquered armour, receives the quiet ceremony of sake from a geisha — power and grace suspended in a single, breathless moment on an ancient throne."
|
description: "A fearsome samurai warlord, clad in magnificent lacquered armour, receives the quiet ceremony of sake from a geisha — power and grace suspended in a single, breathless moment on an ancient throne."
|
||||||
date: 2026-05-08
|
date: 2026-05-08T10:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: false
|
featured: false
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Iron Rain"
|
title: "Iron Rain"
|
||||||
description: "A colossal combat mech holds the intersection — armour scarred by a hundred engagements, missile pods tracking, plasma blade coiled and ready — while Neo-Tokyo dissolves in rain around it and no one dares move."
|
description: "A colossal combat mech holds the intersection — armour scarred by a hundred engagements, missile pods tracking, plasma blade coiled and ready — while Neo-Tokyo dissolves in rain around it and no one dares move."
|
||||||
date: 2026-05-10
|
date: 2026-05-10T18:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: true
|
featured: true
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "The Drowned Queen"
|
title: "The Drowned Queen"
|
||||||
description: "She sails still — centuries dead, cursed to never reach port — her black hull half-fog and half-ruin, her skeletal crew working the rigging in silence, her tattered sails full of a wind that does not exist."
|
description: "She sails still — centuries dead, cursed to never reach port — her black hull half-fog and half-ruin, her skeletal crew working the rigging in silence, her tattered sails full of a wind that does not exist."
|
||||||
date: 2026-05-11
|
date: 2026-05-11T10:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: false
|
featured: false
|
||||||
categories:
|
categories:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.6 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
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: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Mythology
|
||||||
|
- Fantasy
|
||||||
|
tags:
|
||||||
|
- Hades
|
||||||
|
- Underworld
|
||||||
|
- Greek Mythology
|
||||||
|
- Throne
|
||||||
|
- Atmospheric
|
||||||
|
- Horror
|
||||||
|
- Cerberus
|
||||||
|
- Ancient
|
||||||
|
slug: the-empty-throne
|
||||||
|
prompt:
|
||||||
|
scene: "A grand, wide establishing shot of the throne room of Hades — the court of the Underworld rendered in its full architectural enormity — seen from the entrance archway looking in, as if the viewer has just arrived and has not yet been permitted to enter. The throne itself dominates: cut from a single piece of volcanic obsidian, its surfaces mirror-polished to perfection, its form severe and angular with no ornamentation and no concession to comfort — a chair designed to be occupied by something that does not tire, does not age, and does not require any of the things thrones are usually built to communicate. It sits elevated on a three-step dais of black marble, bare. To the right of the throne, a tall narrow window cut through the rock opens onto the only visible exterior: the river Styx, its surface moving with the faint luminescence of a million passing souls, its far bank lost in haze. On the far left of the room, curled in sleep on the bare stone floor beside the dais steps, Cerberus rests — three heads, each in a different direction, two sleeping, one not quite sleeping, its central pair of eyes half-open and reflecting the ambient light. The room is otherwise empty. Everything about its emptiness implies that this state is extremely recent."
|
||||||
|
environment: "The hall is monumental and carved from the living rock of the underworld itself — no dressed stone, no masonry, but the raw basalt of the deep earth shaped by will alone into columns, arches, and vaulted ceilings that disappear into dark heights far above. The columns are not straight: they lean slightly inward, as if the mountain is pressing down. From the ceiling, stalactites of black crystal hang at irregular intervals, each one emitting a faint cold phosphorescent glow — the only ambient light in the room. The floor is smooth as glass and slightly warm — the warmth of the deep earth, not fire. Along the walls, at irregular intervals, shallow niches hold soul-candles: flames that burn without wax, cold blue-white, each one representing a soul that has been here long enough to become part of the furniture. In the far corner, an hourglass the height of a man, its sand moving upward instead of down. From beyond the Styx window, very faint, the sound of water and the murmur of a crowd that never quite resolves into individual voices. Charon's boat is just barely visible on the river surface, moving toward a landing that is out of frame."
|
||||||
|
style: "Greek mythological architecture fused with the geological horror of a deep cave system — the civic grandeur of Minoan palace architecture and the formal severity of classical Greek design, both consumed and transformed by the living earth of the underworld into something pre-architectural, primordial. Lighting: the soul-candles as ambient cold fill, the Styx window as the sole directional source — the faint blue-silver luminescence of the river casting long horizontal shadows from the throne across the floor toward camera. Cerberus rendered with the conviction of a real animal of its size — the weight of three lion-scale heads, the musculature of a creature bred from nothing natural, fur matted with the particular dampness of a cave creature, three distinct personalities readable in three distinct resting postures. The obsidian throne's mirror surface reflecting a distorted, cold image of the empty room back on itself. Reference: the architectural world-building of God of War's Helheim, John Martin's vast apocalyptic paintings, the cave compositions of Caspar David Friedrich. 8K resolution, photorealistic render, maximum depth of field across the full room depth, the far Styx window in crisp focus against the near darkness of the entrance."
|
||||||
|
video: "The room is still. The soul-candles burn without movement. The Styx surface shifts faintly through the window, its light shifting across the throne. One of Cerberus's sleeping heads twitches — an ear rotation, the involuntary response of a dreaming animal. Then the not-quite-sleeping head opens fully. All three heads rise simultaneously. All six eyes orient in the same direction: not toward the throne, not toward the river, not toward any visible point in the room. Directly at the camera. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.0 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "The Masquerade's End"
|
||||||
|
description: "The invitations were sent, the candles were lit, the musicians played, the masks were worn — and one more guest arrived than was invited, in a costume that required no costume, moving through the ballroom with the unhurried patience of something that has attended every party that ever ended this way."
|
||||||
|
date: 2026-05-28T14:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Gothic
|
||||||
|
- Surrealist
|
||||||
|
tags:
|
||||||
|
- Masquerade
|
||||||
|
- Venetian
|
||||||
|
- Edgar Allan Poe
|
||||||
|
- Red Death
|
||||||
|
- Horror
|
||||||
|
- Macabre
|
||||||
|
- Ballroom
|
||||||
|
- Gothic
|
||||||
|
slug: the-masquerades-end
|
||||||
|
prompt:
|
||||||
|
scene: "A wide, sweeping shot of a grand Venetian palazzo ballroom at the precise moment the Red Death moves through it — captured in the surreal, suspended stillness of every other occupant having simultaneously stopped. The ballroom is magnificent: vaulted ceiling forty feet overhead, enormous chandeliers of Murano glass and a thousand candles each, gilded walls hung with enormous oil paintings in ornate frames, a polished marble floor inlaid with a geometric pattern in black and white. The room is filled with perhaps a hundred masquerade guests in costumes of extraordinary richness and fantasy: plague doctors, harlequins, doges, queens, gods, animals, angels, demons, all wearing Venetian masks in every conceivable form — bauta, moretta, volto, colombina — in gilt, in jewel-set porcelain, in feathered black, in painted ivory. Every one of them is frozen mid-motion: a woman mid-turn, her silk skirts arrested in a full spiral that should not hold; a pair of dancers frozen at the peak of a lift; a man mid-laugh with his wine glass raised; a group of four mid-conversation. None of them is looking at what has just entered their midst. Moving through the frozen crowd, touching nothing and no one, is the Red Death: a tall figure in a robe of deep scarlet that has the texture of something organic, wearing a mask that is not a mask — a face of white bone beneath which the eyes are not eyes. On every frozen figure it has already passed, the porcelain mask has developed a single hairline crack. On the nearest figure — a woman in gold — the crack runs from crown to chin."
|
||||||
|
environment: "The palazzo ballroom exists in full Belle Époque Venetian splendour: the ceiling fresco above the chandeliers depicts the gods of Olympus at a feast, the paint still vivid. Along the walls, floor-to-ceiling mirrors in gilt frames multiply the frozen crowd infinitely in both directions — in every reflection, the perspective shifts, and in some of them the Red Death appears in positions it is not occupying in the actual room, as if time moves differently in the glass. At one end of the room, the orchestra is frozen mid-bar: violinists with bows at mid-stroke, the conductor's arm raised and stopped, a cellist leaning forward over the instrument. The music has not ended — it has simply paused, as if the score has a rest that has lasted longer than any rest should. Long tables along the walls hold the untouched supper: crystal, silver, elaborate centrepieces, candelabras, ice sculptures that are still the forms they were carved into, not yet melted. The tall windows behind the orchestra are shuttered in red silk. Through the cracks in the shutters, just visible: Venice at carnival, torchlit, distant, continuing entirely without awareness of what is happening inside."
|
||||||
|
style: "Edgar Allan Poe's The Masque of the Red Death translated into the visual language of Venetian Golden Age painting and Kubrick's compositional control — the palatial grandeur of Tiepolo's ceiling frescoes and Veronese's feast paintings, the formal ceremonial horror of Kubrick's Eyes Wide Shut ballroom sequence, and the supernatural dread of the best Poe adaptation work, all unified in a photorealistic render at maximum atmospheric intensity. The frozen crowd rendered with the hyperreal stasis of a Francis Bacon figure — human form under the extreme pressure of supernatural intervention, fabric and posture locked in positions that physics alone could not maintain. The Red Death as the compositional void around which everything else organises: the eye moves to it despite the overwhelming richness of everything surrounding it, because it is the only figure that is not wrong. The porcelain mask cracks rendered with the precision of a conservation scientist: fine as a spider's thread, catching the candlelight as a white line against the painted porcelain. Lighting: a thousand candles from above as the warm ambient, cold from the mirror reflections as a slightly bluer secondary, the Red Death emitting a near-imperceptible cold light of its own — barely visible, only registering in the shadows it casts. 8K resolution, wide anamorphic lens, deep focus across the full ballroom depth."
|
||||||
|
video: "The candles burn. No music. The frozen crowd maintains every impossible posture without strain. The Red Death moves — slowly, deliberately — between two groups of frozen revellers. As it passes the nearest figure, the hairline crack on her porcelain mask spreads — silently, like frost on glass — from the chin downward across the cheek. The mask does not fall. The Red Death stops. It turns its head — slowly — and faces the camera directly. It has not moved for the rest of the video. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.0 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "The Plague Doctor's Round"
|
||||||
|
description: "He makes his rounds at three in the morning, when the city has finally stopped pretending — lantern in one hand, cane in the other, the beak of his mask packed with dried flowers that no longer help anyone, moving through a city that has surrendered to him completely."
|
||||||
|
date: 2026-05-28T12:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Historical
|
||||||
|
tags:
|
||||||
|
- Plague Doctor
|
||||||
|
- Medieval
|
||||||
|
- Fog
|
||||||
|
- Cobblestone
|
||||||
|
- Macabre
|
||||||
|
- Black Death
|
||||||
|
- Night
|
||||||
|
- Atmospheric
|
||||||
|
slug: the-plague-doctors-round
|
||||||
|
prompt:
|
||||||
|
scene: "A lone plague doctor moves through the absolute centre of a fog-drowned medieval street, captured in a long, cinematic shot that places him small against the vast, crushing architecture of a city that has closed around him. He wears the full canonical plague doctor ensemble with hyper-realistic fidelity: a long waxed leather coat the colour of dried blood, black leather gloves to the elbow, a wide-brimmed hat, and the beak mask — long, curved, ominous, the leather cracked and stained from years of use, the eye-pieces dark amber glass that reflects the lantern light without revealing anything within. In his left hand, a wrought iron lantern with a small, steady candle flame — the only warm light source on the street — casting a moving circle of amber barely two metres in any direction. In his right hand, a long wooden cane with a brass tip, which he uses neither for support nor for pointing: he simply carries it, as he always has. His posture is neither hurried nor hesitant. He moves with the particular gait of someone who has been doing this for so long that purpose and resignation have become indistinguishable."
|
||||||
|
environment: "A medieval European city street at three in the morning — a narrow, close-packed canyon of half-timbered buildings leaning toward each other overhead, their upper storeys almost touching, their facades dark and sealed. Every window is shuttered. Every door is closed. On a dozen of those doors, visible as he passes, a red cross — some freshly painted, some weeks old and fading. The cobblestones are wet from recent rain, catching the lantern light in small, irregular reflections. Fog sits low and heavy, dense enough at street level to swallow his feet entirely, reducing him to a coat and hat and beak floating above a white floor. Further down the street — thirty metres, fifty — the fog thickens until the buildings dissolve into grey nothing. A cat sits on a windowsill, watching him pass, absolutely still. At the far edge of visibility, the silhouette of a church spire against a sky that offers no stars and no moon. Somewhere distant, a bell tolling a single note — not the hour, just a bell that has been ringing all night and has not been stopped because there is no one left to stop it."
|
||||||
|
style: "Dutch Golden Age night-scene painting translated into photorealistic cinema — the lantern-lit darkness of Rembrandt's The Night Watch, the claustrophobic medieval streetscapes of Pieter Bruegel the Elder, the fog-choked atmosphere of a Gustave Doré engraving, all rendered with the narrative weight of a Werner Herzog film. Single-source lantern lighting as the entire visual key: warm amber in a tight radius, the rest of the scene in deep, textured darkness ranging from near-black to the diffuse grey-white of the fog layer. The plague doctor rendered with absolute material fidelity: cracked waxed leather, tarnished brass, the slight condensation on the amber glass eyepieces. Colour grade: near-monochrome, dominated by cold greys and blacks, the lantern's amber flame the only warm chromatic event in the entire frame. The cobblestones rendered wet and individually textured, each one reflecting a miniature lantern. 8K resolution, photorealistic render, anamorphic lens with compressed depth perspective emphasising the tunnel-like quality of the street."
|
||||||
|
video: "He walks. The lantern swings slightly in his hand, sending shadows rocking across the wet cobblestones and the building faces. He stops. He tilts the beak mask slightly upward — the posture of listening. Total silence. He does not move for a long moment. Then he continues walking, the lantern swinging again, the shadows rocking. A window shutter on the first floor closes, very slowly, as he passes. He does not look up. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
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."
|
||||||
|
---
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
baseURL = "https://pivoine.art/"
|
baseURL = "https://pivoine.art/"
|
||||||
languageCode = "en-us"
|
locale = "en-US"
|
||||||
title = "Pivoine"
|
title = "Pivoine"
|
||||||
copyright = "© 2026 Pivoine Editorial"
|
copyright = "© 2026 Pivoine Editorial"
|
||||||
|
|
||||||
@@ -57,6 +57,18 @@ weight = 1
|
|||||||
name = "Imprint"
|
name = "Imprint"
|
||||||
url = "/imprint/"
|
url = "/imprint/"
|
||||||
weight = 2
|
weight = 2
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Dev"
|
||||||
|
url = "https://dev.pivoine.art"
|
||||||
|
weight = 3
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Bar"
|
||||||
|
url = "https://bar.pivoine.art"
|
||||||
|
weight = 4
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Roux"
|
||||||
|
url = "https://roux.pivoine.art"
|
||||||
|
weight = 5
|
||||||
|
|
||||||
[markup.goldmark.renderer]
|
[markup.goldmark.renderer]
|
||||||
unsafe = true
|
unsafe = true
|
||||||
@@ -67,3 +79,6 @@ noClasses = true
|
|||||||
|
|
||||||
[build]
|
[build]
|
||||||
writeStats = true
|
writeStats = true
|
||||||
|
|
||||||
|
[security.exec]
|
||||||
|
allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^npx$', '^tailwindcss$']
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ .Site.LanguageCode }}" class="scroll-smooth">
|
<html lang="{{ .Site.Language.Locale }}" class="scroll-smooth">
|
||||||
<head>
|
<head>
|
||||||
{{- partial "head.html" . -}}
|
{{- partial "head.html" . -}}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -23,7 +23,18 @@
|
|||||||
<ul class="flex flex-wrap gap-x-8 gap-y-4">
|
<ul class="flex flex-wrap gap-x-8 gap-y-4">
|
||||||
{{- range .Site.Menus.footer -}}
|
{{- range .Site.Menus.footer -}}
|
||||||
<li>
|
<li>
|
||||||
|
{{- if hasPrefix .URL "http" -}}
|
||||||
|
<a
|
||||||
|
href="{{ .URL }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="label text-fog hover:text-heat transition-colors"
|
||||||
|
>
|
||||||
|
{{- .Name -}}{{ partial "icon.html" "arrow-external" }}
|
||||||
|
</a>
|
||||||
|
{{- else -}}
|
||||||
<a href="{{ .URL }}" class="label text-fog hover:text-heat transition-colors">{{ .Name }}</a>
|
<a href="{{ .URL }}" class="label text-fog hover:text-heat transition-colors">{{ .Name }}</a>
|
||||||
|
{{- end -}}
|
||||||
</li>
|
</li>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- with .Site.Params.social -}}
|
{{- with .Site.Params.social -}}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta property="og:url" content="{{ .Permalink }}" />
|
<meta property="og:url" content="{{ .Permalink }}" />
|
||||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
<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 -}}
|
{{- $ogImage := .Site.Params.ogImage -}}
|
||||||
{{- $ogImageAlt := .Site.Title -}}
|
{{- $ogImageAlt := .Site.Title -}}
|
||||||
{{- with .Params.banner -}}
|
{{- 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"
|
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>
|
/></noscript>
|
||||||
|
|
||||||
<!-- CSS via Hugo Pipes + PostCSS + Tailwind v4 -->
|
<!-- CSS via Hugo Pipes + Tailwind v4 (native css.TailwindCSS, no PostCSS) -->
|
||||||
{{- $css := resources.Get "css/main.css" | css.PostCSS -}}
|
{{- $css := resources.Get "css/main.css" | css.TailwindCSS -}}
|
||||||
{{- if eq hugo.Environment "production" -}}
|
{{- if eq hugo.Environment "production" -}}
|
||||||
{{- $css = $css | minify | fingerprint "sha256" -}}
|
{{- $css = $css | minify | fingerprint "sha256" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
|
||||||
<!-- ── ARTICLE HEADER ────────────────────────────────────────── -->
|
<!-- ── ARTICLE HEADER ────────────────────────────────────────── -->
|
||||||
<header class="gutter-x pt-36 pb-12 max-w-5xl mx-auto relative overflow-hidden">
|
<header class="gutter-x pt-36 pb-12 relative overflow-hidden">
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
<!-- Faded decorative category text -->
|
<!-- Faded decorative category text -->
|
||||||
{{- with index .Params.categories 0 -}}
|
{{- with index .Params.categories 0 -}}
|
||||||
<div
|
<div
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
<time class="label text-fog" datetime="{{ .Date.Format "2006-01-02" }}">
|
<time class="label text-fog" datetime="{{ .Date.Format "2006-01-02" }}">
|
||||||
{{- .Date.Format "January 2, 2006" -}}
|
{{- .Date.Format "January 2, 2006" -}}
|
||||||
</time>
|
</time>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- ── GRADIENT DIVIDER ──────────────────────────────────────── -->
|
<!-- ── GRADIENT DIVIDER ──────────────────────────────────────── -->
|
||||||
@@ -146,11 +148,13 @@
|
|||||||
<!-- ── TAGS ─────────────────────────────────────────────────── -->
|
<!-- ── TAGS ─────────────────────────────────────────────────── -->
|
||||||
{{- with .Params.tags -}}
|
{{- with .Params.tags -}}
|
||||||
<footer class="gutter-x border-t border-zinc pt-10 pb-20">
|
<footer class="gutter-x border-t border-zinc pt-10 pb-20">
|
||||||
<div class="max-w-5xl mx-auto flex flex-wrap gap-3 items-center">
|
<div class="max-w-5xl mx-auto flex no-wrap gap-3 items-start">
|
||||||
<span class="label text-fog">Tagged:</span>
|
<span class="label text-fog">Tagged:</span>
|
||||||
|
<span class="flex flex-wrap gap-3">
|
||||||
{{- range . -}}
|
{{- range . -}}
|
||||||
<a href="/tags/{{ . | urlize }}/" class="badge badge-outline"># {{ . }}</a>
|
<a href="/tags/{{ . | urlize }}/" class="badge badge-outline"># {{ . }}</a>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/rss+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/rss+xml image/svg+xml;
|
||||||
|
|
||||||
|
open_file_cache max=1000 inactive=30s;
|
||||||
|
open_file_cache_valid 60s;
|
||||||
|
open_file_cache_min_uses 2;
|
||||||
|
open_file_cache_errors on;
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|||||||
+1
-4
@@ -9,10 +9,7 @@
|
|||||||
"format:check": "prettier --check \"layouts/**/*.html\" \"assets/**/*.css\" \"assets/**/*.js\" \"content/**/*.md\" \"*.toml\""
|
"format:check": "prettier --check \"layouts/**/*.html\" \"assets/**/*.css\" \"assets/**/*.js\" \"content/**/*.md\" \"*.toml\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"@tailwindcss/cli": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.0",
|
|
||||||
"postcss": "^8.5.0",
|
|
||||||
"postcss-cli": "^11.0.0",
|
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"prettier-plugin-go-template": "^0.0.15",
|
"prettier-plugin-go-template": "^0.0.15",
|
||||||
"prettier-plugin-toml": "^2.0.6",
|
"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