Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Clone this repository, or open the directory of the example you want. Each examp
- [in-app-video-creation](examples/in-app-video-creation) lets a user create the same promo video three ways: an embedded Studio SDK editor, a quick form, and a one-click headless render, all through one render proxy that keeps the API key server-side. Companion code for [Add video creation to your app without building an editor](https://shotstack.io/learn/add-video-creation-to-your-app/).
- [instagram-ai-video](examples/instagram-ai-video) generates a script, voiceover and background image with AI, renders a 1080x1920 video, and publishes it as an Instagram Reel. Companion code for [How to automate Instagram posts with AI video](https://shotstack.io/learn/automate-instagram-posts-with-ai-video/).
- [multi-client-video-automation](examples/multi-client-video-automation) renders branded promo videos for three clients in three aspect ratios from one master template, and records which render belongs to which client. Companion code for [A guide to automating video content production for multiple clients](https://shotstack.io/learn/automating-video-production-multiple-clients/).
- [news-article-videos](examples/news-article-videos) renders one vertical video per article from one fixed template: a headline and two images filled in with merge fields. The finished video URL is written back onto the article, so a rerun only renders new stories.
- [rapidreels](examples/rapidreels) creates faceless short-form videos using generative AI. [View demo](https://shotstack.io/demos/social-media-video-maker/).
- [reelestate](examples/reelestate) turns static real estate images into fully edited video slideshows. [View demo](https://shotstack.io/demos/real-estate-video-listing-maker/).

Expand Down
10 changes: 10 additions & 0 deletions examples/news-article-videos/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Your Shotstack sandbox API key: https://dashboard.shotstack.io/register
SHOTSTACK_API_KEY=

# Optional. stage (sandbox, default) or v1 (production). Use the key from the same environment.
# Both keys are in the dashboard under API Keys: https://dashboard.shotstack.io/
SHOTSTACK_ENV=

# Optional. An HTTPS URL on your server. Shotstack posts to it when each render finishes.
# https://shotstack.io/docs/guide/architecting-an-application/webhooks/
CALLBACK_URL=
2 changes: 2 additions & 0 deletions examples/news-article-videos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
renders.jsonl
77 changes: 77 additions & 0 deletions examples/news-article-videos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# News article videos

Render one vertical video for each article a newsroom publishes. Each article record has a
headline and two images. One template holds the design. The script fills the template with merge
fields and submits one render per article. When a render finishes, the script writes the video URL
back onto the article, so a rerun only renders new stories. You get one 24 second 1080 x 1920 MP4
per article.

## Requirements

- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key
- Node.js 20 or later

Sandbox renders are watermarked. Your account needs at least one credit to use the sandbox.

## Setup

```bash
git clone https://github.com/shotstack/shotstack-cookbook.git
cd shotstack-cookbook/examples/news-article-videos
```

Copy the environment file. Add your sandbox key to `.env`.

```bash
cp .env.example .env
```

Load the file into your shell. Do this in each new terminal:

```bash
set -a
source .env
set +a
```

## Run

Submit one render per article:

```bash
node render.mjs
```

Then check them:

```bash
node status.mjs
```

Run `status.mjs` again until each render shows `done`.

## What happens

`render.mjs` reads `edit.json` once and `articles.json` once. It checks that each article has a
slug, a headline and two HTTPS image URLs. It reports every problem it finds, then stops. It skips
articles that already have a `videoUrl`. For each remaining article it adds a `merge` array to the
template and submits one render. The first image shows for 12 seconds with a slow zoom. The second
image fades in for the next 12 seconds. The headline rises in over a dark panel at the bottom. A
music track plays under the whole video. The script appends one line per render to
`renders.jsonl` and ends with the count of submitted renders.

`status.mjs` reads `renders.jsonl` and checks each render once. A `done` render prints its video
URL. The script then writes each finished URL into `articles.json` as `videoUrl`. In production,
read the articles from your CMS and write the URL back through its API. A sandbox URL expires
after 24 hours.

To receive a webhook instead of polling, set `CALLBACK_URL` in `.env` to an HTTPS URL on your
server. Shotstack posts to it when each render finishes.

To render your own stories, replace the records in `articles.json`. Image URLs must be public
HTTPS URLs. To change the design, edit `edit.json` and keep the `{{PLACEHOLDER}}` tokens.

`renders.jsonl` only appends. To start a new batch, delete the file and remove the `videoUrl`
fields from `articles.json`.

To render in production, set `SHOTSTACK_ENV=v1` and put your production key in `.env`.
88 changes: 88 additions & 0 deletions examples/news-article-videos/api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const API_KEY = process.env.SHOTSTACK_API_KEY;
const ENV = process.env.SHOTSTACK_ENV || 'stage';
const API = `https://api.shotstack.io/edit/${ENV}`;
const TIMEOUT_MS = 30_000;

export function fail(message) {
console.error(message);
process.exit(1);
}

export function requireConfig() {
if (!API_KEY) {
fail('Set SHOTSTACK_API_KEY before you run this script.');
}

if (!['stage', 'v1'].includes(ENV)) {
fail('SHOTSTACK_ENV must be stage or v1.');
}
}

// A rejected key or a dead network fails every record the same way. The
// caller stops the batch at the first one instead of printing a line per record.
function stopBatch(message) {
return Object.assign(new Error(message), { fatal: true });
}

async function apiError(response) {
const text = await response.text();

try {
const body = JSON.parse(text);
return (
body.errors?.[0]?.detail ?? body.response?.error ?? body.message ?? text
);
} catch {
return text;
}
}

async function request(path, options, label) {
let response;

try {
response = await fetch(`${API}${path}`, {
...options,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
signal: AbortSignal.timeout(TIMEOUT_MS)
});
} catch {
throw stopBatch(
`${label}: the network request failed. Check your connection and run again.`
);
}

if (response.status === 401 || response.status === 403) {
throw stopBatch(
`${label}: the API rejected the key (${response.status}). Check SHOTSTACK_API_KEY and SHOTSTACK_ENV.`
);
}

if (!response.ok) {
throw new Error(`${label}: ${response.status} ${await apiError(response)}`);
}

return (await response.json()).response;
}

export async function submitRender(edit, label) {
const { id } = await request(
'/render',
{ method: 'POST', body: JSON.stringify(edit) },
label
);

if (!id) {
throw new Error(`${label}: the render response did not contain an id.`);
}

return id;
}

export function getRender(renderId, label) {
return request(`/render/${renderId}`, { method: 'GET' }, label);
}
29 changes: 29 additions & 0 deletions examples/news-article-videos/articles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[
{
"slug": "harbour-beaches-closed",
"headline": "Big swell closes harbour beaches for the weekend",
"images": [
"https://shotstack-assets.s3.amazonaws.com/images/wave-barrel.jpg",
"https://shotstack-assets.s3.amazonaws.com/images/slideshow2.jpeg"
],
"videoUrl": "https://shotstack-api-stage-output.s3-ap-southeast-2.amazonaws.com/m0018rioc2/8e0a34d4-cb39-434f-b723-d44a50f7365a.mp4"
},
{
"slug": "mountain-road-reopens",
"headline": "Mountain road reopens six weeks after the landslide",
"images": [
"https://shotstack-assets.s3.amazonaws.com/images/slideshow7.jpeg",
"https://shotstack-assets.s3.amazonaws.com/images/slideshow4.jpeg"
],
"videoUrl": "https://shotstack-api-stage-output.s3-ap-southeast-2.amazonaws.com/m0018rioc2/7bc220e2-db31-43f8-b403-d4e5995a9cf5.mp4"
},
{
"slug": "rates-fall-again",
"headline": "Home loan rates fall for the third month in a row",
"images": [
"https://shotstack-assets.s3.amazonaws.com/images/financial-background.jpg",
"https://shotstack-assets.s3.amazonaws.com/images/business-man.jpg"
],
"videoUrl": "https://shotstack-api-stage-output.s3-ap-southeast-2.amazonaws.com/m0018rioc2/0f30a9ca-22b0-48dd-b928-a443ee70cdec.mp4"
}
]
81 changes: 81 additions & 0 deletions examples/news-article-videos/edit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"timeline": {
"background": "#000000",
"fonts": [
{
"src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf"
}
],
"tracks": [
{
"clips": [
{
"asset": {
"type": "rich-text",
"text": "{{HEADLINE}}",
"font": {
"family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c",
"size": 72,
"weight": "800",
"color": "#ffffff"
},
"style": { "lineHeight": 1.1 },
"background": {
"color": "#0b0f1a",
"opacity": 0.82,
"borderRadius": 24
},
"padding": { "top": 36, "right": 44, "bottom": 44, "left": 44 },
"align": { "horizontal": "left", "vertical": "middle" },
"animation": {
"preset": "ascend",
"duration": 0.6,
"direction": "up"
}
},
"start": 0.5,
"length": 23.5,
"width": 960,
"height": 420,
"position": "bottom",
"offset": { "y": 0.08 }
}
]
},
{
"clips": [
{
"asset": { "type": "image", "src": "{{IMAGE_1}}" },
"start": 0,
"length": 12,
"fit": "crop",
"effect": "zoomInSlow"
},
{
"asset": { "type": "image", "src": "{{IMAGE_2}}" },
"start": 12,
"length": 12,
"fit": "crop",
"effect": "slideUpSlow",
"transition": { "in": "fade" }
}
]
},
{
"clips": [
{
"asset": {
"type": "audio",
"src": "https://shotstack-assets.s3.amazonaws.com/music/unminus/berlin.mp3",
"volume": 0.3,
"effect": "fadeOut"
},
"start": 0,
"length": "end"
}
]
}
]
},
"output": { "format": "mp4", "size": { "width": 1080, "height": 1920 } }
}
Loading
Loading