A small library to help you create a stills generator.
You're going to need:
- brew: https://brew.sh/
- imagemagick:
brew install imagemagick
- ffmpeg:
brew install ffmpeg
npm install stills --save
const {
sources,
content,
filters,
destinations,
validators,
generate,
taggers
} = require('stills');
const { resolve } = require('path');
const config = {
source: new sources.S3({
accessKeyId: S3_ACCESS_KEY_ID,
secretAccessKey: S3_SECRET_ACCESS_KEY,
bucket: S3_BUCKET
}),
content: new content.Still(),
filters: [
new filters.Captions({
folder: resolve('./captions')
})
],
validators: [new validators.FaceDetection()],
getPostText: (filterOutput) => (filterOutput.captions || []).join('\n'),
taggers: [
new taggers.Episode(),
new taggers.Static({
tags: ['Hello']
})
],
destinations: [
new destinations.Tumblr({
consumerKey: TUMBLR_CONSUMER_KEY,
consumerSecret: TUMBLR_CONSUMER_SECRET,
token: TUMBLR_ACCESS_TOKEN_KEY,
tokenSecret: TUMBLR_ACCESS_TOKEN_SECRET,
blogName: TUMBLR_BLOG_NAME,
tags: ['Hello']
})
]
};
generate(config);
This will generate a random still from your S3 bucket, make sure at least one face appears in the image, add a caption from a random SRT file in the captions
folder and then upload the result to Tumblr.
There are other options for source
, content
and validators
. Keep readin'.
In all likelihood you'll never use this, but if you're doing more advanced things, like posting multiple images in a single one run, or immediately reblogging the post with a different caption, you can use generateChain
. This allowa you to execute multiple configs in a series. The configs can either be simple config objects like the standard case above, or a function that returns a config object.
await generateChain([
firstConfig,
(previousResult, allPreviousResults) => {
return secondConfig;
}
]);
The previousResult
parameter passed in is the result of the previous run, which is essentially just whatever the filters return. Can sometimes come in handy. Example previousResult
:
{
"filters": {
"captions": ["Welcome to Flavortown!", "Please stay a while"]
}
}
If you don't want to spend rack up your S3 bill while testing, you can point the generator at a local folder containing videos.
new stills.sources.Local({
folder: resolve('./videos')
});
As you saw in the example above, you can point the generator at an S3 bucket. It won't download the full video, as ffmpeg
is capable of working with remote files.
new stills.sources.S3({
accessKeyId: S3_ACCESS_KEY_ID,
secretAccessKey: S3_SECRET_ACCESS_KEY,
bucket: S3_BUCKET
});
This one doesn't have any options. It's just:
new stills.content.Still();
Most sites have some upper limit on GIF sizes, so you can adjust either the generated image width
(default: 540
), the duration
(default: 2
) or the frame-rate with fps
(default: 12
) to fit your needs.
new stills.content.Gif({
width: 540,
duration: 2,
fps: 12
});
This filter supports reading random .srt
files (movie subtitles) and .txt
files (newline-separated lines) from a folder and applying them to either a still and the GIF.
new stills.filters.Captions({
folder: resolve('./captions'),
num: 1,
isSequential: false,
font: resolve('./fonts/maison.ttf')
background: null,
});
The parameters:
folder
(mandatory) is the location from which the subtitles files will be loaded.num
(default:1
) is only applicable to GIFs; it determines how many captions to apply, distributed evenly based on the GIF duration.isSequential
(default:false
) is relevant whennum > 1
, determines whether the randomly picked captions are be sequential.font
(default:null
) is a path to a font file, will use Arial if nothing is provided.background
(default:null
) is an optional hex rgba color that will be added under the text, will use a text drop-shadow if nothing is provided.
Destroys your content by mangling random bytes (default: 300
).
new stills.filters.Glitch({
bytes: 300
});
Stretches your image; for a still, it will immediately apply the stretch, for a GIF, it will increasingly apply the transformation during the course of the animation.
widthFactor
and heightFactor
should be 0-1; the higher the number, the more it streches the image in that direction.
new stills.filters.Distortion({
heightFactor: 0.6
widthFactor: 0
});
Only works with GIFs! Detect a face from the last frame and zooms towards it throughout the GIF. If it can't find a face or if you give it a still image, it does nothing.
new stills.filters.FaceZoom({
startPosition: 0.5,
lastFrameDelayMs: null
});
By default it starts zooming towards the face half-way through the GIF, but you can adjust that using startPosition
(0-1). You can also add a delay (in ms) to the last zoomed in frame with lastFrameDelayMs
(default: null
)
Applies a "max" between frames in your image, essentially leading it towards blacks and making it look like it's melting. No options.
new stills.filters.Melt();
If you watched Twin Peaks - The Return, you might be able to guess what this does. If not, I guess just try it and see what happens.
new stills.filters.Station();
Shuffle the frames in a GIF.
new stills.filters.Shuffle();
Stutter a bunch of frames in a GIF. Turns normal frames like this:
1 2 3 4 5 6 7 8 9
to this:
1 2 3 4 3 4 3 4 9
new stills.filters.Stutter({
startFrame: null,
numFrames: 6,
stutterLength: 2,
stutterDelay: null
});
startFrame
: where to start stuttering. Picks a random frame ifnull
.numFrames
: number of frames to stutter for / replace with stutter.stutterLength
: number of frames to repeat. The above example with 3 would look like "1 2 3 4 5 3 4 5 8 9"stutterDelay
: delay of the stuttered frame in ms. Will just use the original delay ifnull
.
Taggers help assemble the list of tags that are added to posts when sent to destinations.
new taggers.Episode();
Passes the name of the input file (from your source
) as a tag.
new taggers.Static({
tags: ['Hello']
});
Simply passes along whatever you specify in tags
.
new taggers.Captions();
If you've used the caption filter, it'll extract nouns and other tag-friendly words to use as tags.
What it says on the box.
new stills.destinations.Tumblr({
consumerKey: TUMBLR_CONSUMER_KEY,
consumerSecret: TUMBLR_CONSUMER_SECRET,
token: TUMBLR_ACCESS_TOKEN_KEY,
tokenSecret: TUMBLR_ACCESS_TOKEN_SECRET,
blogName: TUMBLR_BLOG_NAME,
tags: ['Hello'],
isIncludeText: true,
},
});
The name of the video file is automatically added as a tag to the post, but you can also provide additional ones with the tags
parameter (default: []
).
You can add some text to the Tumblr if you set isIncludeText
to true
in each individual destination, and pass in a getPostText
method into the generate
call. An object will be passed into getPostText
method with the output from the various filters that are applied (key is the name of the filter).
When a validator fails, it deletes the generated image and creates another one to try again. It does this a maximum of 10 times, and then gives up and posts whatever.
Using Tensorflow, makes sure there's at least one face in the image.
new stills.validators.FaceDetection();
If you'd like to provide any additional plugins (like sources or filters), please fork the repo and open a PR!