-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit b1b0846
Showing
8 changed files
with
1,759 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.