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.
@@ -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
@@ -31,6 +31,7 @@ mediasnacks <command> <args>
31
31
 
32
32
  - `resize` Resizes videos or images
33
33
  - `edgespic` Extracts first and last frames
34
+ - `ssim` Computes similarity of two images
34
35
  - `gif`: Video to GIF
35
36
 
36
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
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 { deepEqual } from 'node:assert/strict'
3
+ import { ok } from 'node:assert/strict'
4
4
 
5
- import { videoAttrs } from './utils/ffmpeg.js'
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
- deepEqual(
15
- await videoAttrs(join(tmp, 'lenna.avif')),
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'],
@@ -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 { cli, mkTempDir, sha1 } from './utils/test-utils.js'
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 generated = join(tmp, 'edgespic', '60fps_first.png')
24
- equal(sha1(generated), sha1(fixture))
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 generated = join(tmp, 'edgespic', '60fps_last.png')
30
- equal(sha1(generated), sha1(fixture))
32
+ const similarityScore = await ssim(out, fixture)
33
+ ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
31
34
  })
32
35
  })
36
+
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
+ })
@@ -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) => {
@@ -1,7 +1,7 @@
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'
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)