mediasnacks 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.zsh/completions/_mediasnacks +2 -1
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/avif.test.js +4 -7
- package/src/cli.js +1 -0
- package/src/edgespic.test.js +12 -8
- package/src/fixtures/edgespic/60fps_first.png +0 -0
- package/src/fixtures/edgespic/60fps_last.png +0 -0
- package/src/fixtures/lenna.avif +0 -0
- package/src/ssim.js +44 -0
- package/src/utils/ffmpeg.js +3 -4
- package/src/utils/test-utils.js +4 -4
|
@@ -6,6 +6,7 @@ _mediasnacks_commands=(
|
|
|
6
6
|
|
|
7
7
|
'resize:Resizes videos or images'
|
|
8
8
|
'edgespic:Extracts first and last frames'
|
|
9
|
+
'ssim:Computes similarity of two images'
|
|
9
10
|
'gif:Video to GIF'
|
|
10
11
|
|
|
11
12
|
'detectdups:Detects sequentially duplicate frames in a video'
|
|
@@ -39,7 +40,7 @@ fi
|
|
|
39
40
|
|
|
40
41
|
local cmd="$words[2]"
|
|
41
42
|
case "$cmd" in
|
|
42
|
-
avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores)
|
|
43
|
+
avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores|ssim)
|
|
43
44
|
_files
|
|
44
45
|
;;
|
|
45
46
|
qdir)
|
package/README.md
CHANGED
package/package.json
CHANGED
package/src/avif.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import {
|
|
3
|
+
import { ok } from 'node:assert/strict'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ssim } from './ssim.js'
|
|
6
6
|
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
7
7
|
|
|
8
8
|
const rel = f => join(import.meta.dirname, f)
|
|
@@ -11,9 +11,6 @@ test('PNG to AVIF', async () => {
|
|
|
11
11
|
const tmp = mkTempDir('avif')
|
|
12
12
|
cli('avif', '--output-dir', tmp, rel('fixtures/lenna.png'))
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await videoAttrs(rel('fixtures/lenna.avif')))
|
|
17
|
-
// That's because we use non-deterministic avif.
|
|
18
|
-
// Claude says: avif is deterministic only when it's single-threaded: '-threads 1'
|
|
14
|
+
const similarityScore = await ssim(join(tmp, 'lenna.avif'), rel('fixtures/lenna.avif'))
|
|
15
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
19
16
|
})
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ const COMMANDS = {
|
|
|
12
12
|
|
|
13
13
|
resize: ['resize.js', 'Resizes videos or images'],
|
|
14
14
|
edgespic: ['edgespic.js', 'Extracts first and last frames'],
|
|
15
|
+
ssim: ['ssim.js', 'Computes SSIM between two images'],
|
|
15
16
|
gif: ['gif.sh', 'Video to GIF\n'],
|
|
16
17
|
|
|
17
18
|
detectdups: ['detectdups.js', 'Detects duplicate frames in a video'],
|
package/src/edgespic.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { ok } from 'node:assert/strict'
|
|
1
2
|
import { join } from 'node:path'
|
|
2
|
-
import { ok, equal } from 'node:assert/strict'
|
|
3
3
|
import { describe, test } from 'node:test'
|
|
4
4
|
import { cpSync, readdirSync, } from 'node:fs'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { ssim } from './ssim.js'
|
|
7
|
+
import { cli, mkTempDir } from './utils/test-utils.js'
|
|
7
8
|
|
|
8
9
|
const rel = f => join(import.meta.dirname, f)
|
|
9
10
|
|
|
@@ -18,15 +19,18 @@ describe('edgespic', () => {
|
|
|
18
19
|
ok(files.length === 2, `Expected 2 PNG files, got ${files.length}`)
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
test('extracts first frame', () => {
|
|
22
|
+
test('extracts first frame', async () => {
|
|
23
|
+
const out = join(tmp, 'edgespic', '60fps_first.png')
|
|
22
24
|
const fixture = rel('fixtures/edgespic/60fps_first.png')
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const similarityScore = await ssim(out, fixture)
|
|
26
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
25
27
|
})
|
|
26
28
|
|
|
27
|
-
test('extracts last frame', () => {
|
|
29
|
+
test('extracts last frame', async () => {
|
|
30
|
+
const out = join(tmp, 'edgespic', '60fps_last.png')
|
|
28
31
|
const fixture = rel('fixtures/edgespic/60fps_last.png')
|
|
29
|
-
const
|
|
30
|
-
|
|
32
|
+
const similarityScore = await ssim(out, fixture)
|
|
33
|
+
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
31
34
|
})
|
|
32
35
|
})
|
|
36
|
+
|
|
Binary file
|
|
Binary file
|
package/src/fixtures/lenna.avif
CHANGED
|
Binary file
|
package/src/ssim.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const MAN = `
|
|
7
|
+
SYNOPSIS
|
|
8
|
+
mediasnacks ssim <img1> <img2>
|
|
9
|
+
|
|
10
|
+
DESCRIPTION
|
|
11
|
+
Computes the Structural Similarity Index (SSIM) between two images using ffmpeg.
|
|
12
|
+
`.trim()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const [img1, img2] = process.argv.slice(2)
|
|
17
|
+
if (!img1 || !img2) {
|
|
18
|
+
console.log(MAN)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const score = await ssim(img1, img2)
|
|
23
|
+
console.log(score)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function ssim(img1, img2) {
|
|
27
|
+
const result = await ffmpeg([
|
|
28
|
+
'-i', img1,
|
|
29
|
+
'-i', img2,
|
|
30
|
+
'-filter_complex', 'ssim',
|
|
31
|
+
'-f', 'null', '-'
|
|
32
|
+
])
|
|
33
|
+
const match = result.stderr.match(/All:([\d.]+)/)
|
|
34
|
+
if (!match)
|
|
35
|
+
throw new Error(`Could not parse SSIM output:\n${result.stderr}`)
|
|
36
|
+
return parseFloat(match[1])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if (import.meta.main)
|
|
41
|
+
main().catch(err => {
|
|
42
|
+
console.error(err.message)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
})
|
package/src/utils/ffmpeg.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
export async function ffmpeg(args) {
|
|
5
|
-
return runSilently('ffmpeg', args)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
export async function assertUserHasFFmpeg() {
|
|
9
5
|
try {
|
|
10
6
|
await runSilently('ffmpeg', ['-version'])
|
|
@@ -15,6 +11,9 @@ export async function assertUserHasFFmpeg() {
|
|
|
15
11
|
}
|
|
16
12
|
}
|
|
17
13
|
|
|
14
|
+
export async function ffmpeg(args) {
|
|
15
|
+
return runSilently('ffmpeg', args)
|
|
16
|
+
}
|
|
18
17
|
|
|
19
18
|
async function runSilently(program, args) {
|
|
20
19
|
return new Promise((resolve, reject) => {
|
package/src/utils/test-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { join }
|
|
2
|
-
import { tmpdir }
|
|
3
|
-
import { spawnSync }
|
|
4
|
-
import { createHash }
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
5
|
import { mkdtempSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
6
6
|
|
|
7
7
|
const rel = f => join(import.meta.dirname, f)
|