Skip to content

Commit

Permalink
initial checkin
Browse files Browse the repository at this point in the history
  • Loading branch information
moos committed Jan 27, 2019
0 parents commit b1b0846
Show file tree
Hide file tree
Showing 8 changed files with 1,759 additions and 0 deletions.
Empty file added LICENSE
Empty file.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
davinci-slide-builder
---------------

Generates an `fcpxml` (1.8) formatted timeline of stills that can be imported into [Da Vinci Resolve](https://www.blackmagicdesign.com/products/davinciresolve/) to make a slideshow.

### Motivation
Da Vinci Resolve (free version) is an excellent software for video editing, rivaling both Final Cut Pro and Adobe Premier Pro. Alas I found it a little limiting in auto generating a slideshow of stills, specifically in being able to sort the media (say randomly) or choosing varied duration time and/or transition effects.


### Install it
Get [node.js](https://nodejs.org/en/), then
```
npm i -g davinci-slide-builder
```

### Example
```
$ slide-builder -d 3 -t 1 ~/pics/2005/img100*.jpg ~/pics/2006/img100*.jpg > out.xml
$ cat out.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fcpxml>
<fcpxml version="1.8">
<resources>
<asset id="sb1" src="file://localhost/c:/Users/foo/pics/2005/img100.jpg" name="img100.jpg" />
<asset id="sb1" src="file://localhost/c:/Users/foo/pics/2005/img101.jpg" name="img101.jpg" />
<asset id="sb1" src="file://localhost/c:/Users/foo/pics/2005/img102.jpg" name="img102.jpg" />
<asset id="sb2" src="file://localhost/c:/Users/foo/pics/2006/img100.jpg" name="img100.jpg" />
</resources>
<library>
<event name="slideshow #1">
<project name="slideshow timeline">
<sequence>
<spine>
<video ref="sb1" duration="75/24s" offset="0/24s" />
<transition duration="12/24s" offset="69/24s" />
<video ref="sb2" duration="70/24s" offset="75/24s" />
<transition duration="12/24s" offset="139/24s" />
<video ref="sb3" duration="66/24s" offset="145/24s" />
<transition duration="12/24s" offset="205/24s" />
<video ref="sb4" duration="69/24s" offset="211/24s" />
<transition duration="12/24s" offset="274/24s" />
</spine>
</sequence>
</project>
</event>
</library>
</fcpxml>
```
Then open _Da Vinci Resolve_, (new project) and "import timeline" from `out.xml`.

To shuffle slides:
```
$ slide-builder -d 3 -t 1 --sort rand ~/pics/2005/img100*.jpg ~/pics/2006/img100*.jpg > out.xml
```

### CLI options
```
$ slide-builder
Usage: slide-builder [options] files...
Options
--duration, -d N - duration of each slide (default: 5 sec)
--transitionDuration, -t N - duration of transition between each slide (default: 1 sec)
0 for no transition.
--randomize, -R [type] - randomize duration of each slide (default: false)
[type] is the random distribution: linear, normal (default: linear)
--range, -r min,max - range of random durations (default: 3,6 secs)
--name S - name of project
(Use --advanced to show less-used options)
files... can be one or more glob patterns, e.g., "pics/slideshow/** pics/summer/**.jpg".
(Use --globs to show more examples)
```
Note: options that are WIP: "--rand normal" and "--sort name".

### Change log
- 1.0.0 Initial version supporting fcpxml 1.8

### License
This software is released under the terms of the MIT license.
151 changes: 151 additions & 0 deletions cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env node

const argv = require('yargs-parser')(process.argv.slice(2), {
boolean: ['dry-run']
});
const fg = require('fast-glob');
const arraySort = require('array-sort');
const builder = require('../src').build;

const defaultIgnores = [
'**/Thumbs.db',
'**/*.dat',
'**/*.ini',
'**/*.sfk',
'**/*.cxf',
'**/*.mxf',
'**/*.THM',
'**/*.txt',
// '**/*.avi', // ???
'**/*.xmp'
];

argv.advanced && advHelp();
argv.globs && globHelp();
argv._.length === 0 && help();

function help() {
console.log(`
Usage: slide-builder [options] files...
Options
--duration, -d N - duration of each slide (default: 5 sec)
--transitionDuration, -t N - duration of transition between each slide (default: 1 sec)
0 for no transition.
--randomize, -R [type] - randomize duration of each slide (default: false)
[type] is the random distribution: linear, normal (default: linear)
--range, -r min,max - range of random durations (default: 3,6 secs)
--name S - name of project
(Use --advanced to show less-used options)
files... can be one or more glob patterns, e.g., "pics/slideshow/** pics/summer/**.jpg".
(Use --globs to show more examples)
`);
process.exit(0);
}

function advHelp() {
console.log(`
Advanced options:
--ext [list] - limit glob to list of file extension, e.g., "jpg,png"
--noext [list] - file extensions to exclude
--ignore [list] - list of glob patterns to ignore, e.g., "node_module/**,backups/**"
--depth - set glob depth, i.e., how deep directories are traversed (default: 3)
--sort [by] - sort entries: [by] is one of:
date - creation date
mdate - modified date
size - file size
name - file name (default)
path - file path
rand - randomize
--rsort [by] - reverse sort order
--dry-run - just show selected files
`);
process.exit(0);
}

function globHelp() {
console.log(`
Glob examples:
pics/foo/* (all files in pics/foo)
pics/foo/*.png (only png files in pics/foo)
pics/foo/** (all files under pics/foo/... recursively)
pics/**/{2015..2017}* (all files under pics/... beginning with "2015", "2016", or "2017")
pics/**/201[5..7]* (same as above using RexEx brackets single character expansion)
pics/**/{2015,2017}* (all files under pics/... beginning with "2015" or "2017")
pics/[[:alpha:][:digit:]]* (app files in pics/ beginning with a letter and number, e.g., "p5")
!private/** (exclusion)
More at https://github.com/isaacs/node-glob#glob-primer
`);
process.exit(0);
}

let toArr = (str) => str ? str.split(',') : [];

let options = {};
Object.entries({
projName: [argv.name],
durationFixed: [argv.duration || argv.d, (a) => parseFloat(a, 10)],
durationRange: [argv.range || argv.r, (a) => toArr(a).map(parseFloat)],
durationRand: [argv.randomize || argv.R, (a) => !!a],
durationDist: [argv.randomize || argv.R],
transitionDuration: [argv.transitionDuration || argv.t,,true],
shuffle: [argv.sort || argv.rsort, (a) => a === 'rand']
}).forEach(([key, [value, proc, falsyOK]]) => {
if (value || falsyOK) options[key] = proc ? proc(value) : value;
});

if (options.durationDist === true) delete options.durationDist;

// process file globs
// const entries = fg.sync(['src/**/*.js', '!src/**/*.spec.js']);
let assets = fg.sync(argv._, {
absolute: true,
deep: argv.depth || 3,
onlyFiles: true,
stats: true,
ignore: [].concat(
defaultIgnores,
toArr(argv.ignore),
toArr(argv.noext).map(ext => '**/*.' + ext)
)
});

// sort
let sort = argv.sort || argv.rsort;
let sortMap = {
date: 'ctime',
mdate: 'mtime',
size: 'size',
name: 'path',
path: 'path'
};

if (sort && !options.shuffle && sortMap[sort]) {

// TODO name should sort based on filename only!

assets = arraySort(assets, sortMap[sort], {
reverse: !!argv.rsort
})
}

assets = assets.map(a => a.path);

// filter extensions
if (argv.ext) {
let exts = argv.ext.split(',');
let extMatch = (path) => exts.filter(ext => path.endsWith('.' + ext)).length;
assets = assets.filter(extMatch);
}

if (argv.dryRun) {
console.log(assets);
console.log(`${assets.length} matches`);
process.exit(0);
}

let out = builder(assets, options);
console.log(out)
Loading

0 comments on commit b1b0846

Please sign in to comment.