@ossy/media-tasks 3.2.0 → 3.4.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/README.md +9 -2
- package/package.json +5 -3
- package/src/encode-blurhash.js +37 -0
- package/src/encode-blurhash.spec.js +29 -0
- package/src/resize-common-web.task.js +26 -3
package/README.md
CHANGED
|
@@ -8,9 +8,15 @@ Tasks in this package are registered with `TaskService.RegisterTask()` at API st
|
|
|
8
8
|
|
|
9
9
|
| Task id | Trigger | Description |
|
|
10
10
|
|---------|---------|-------------|
|
|
11
|
-
| `@ossy/media-tasks/tasks/resize-common-web` | `@ossy/platform/schema/file` `Created` (`content.ContentType: image/*`) | Generates common web thumbnail and gallery sizes using `sharp` |
|
|
11
|
+
| `@ossy/media-tasks/tasks/resize-common-web` | `@ossy/platform/schema/file` `Created` (`content.ContentType: image/*`) | Generates common web thumbnail and gallery sizes using `sharp`, then patches `content.blurhash` / `content.width` / `content.height` |
|
|
12
12
|
| `@ossy/media-tasks/tasks/visual-content-descriptors` | `@ossy/platform/schema/file` `Created` (`image/*`, `video/*`) | Uses GPT-4o to generate title, description, tags, and alt text |
|
|
13
13
|
|
|
14
|
+
## Image placeholders (BlurHash)
|
|
15
|
+
|
|
16
|
+
After derivatives upload, `resize-common-web` encodes a [BlurHash](https://blurha.sh/) string and natural pixel size onto the file resource via `@ossy/resources/actions/patch-content` (ADR 0008 `Patched` deep merge — does not replace `sizes`).
|
|
17
|
+
|
|
18
|
+
Consumers (`Image` / `ImageCard`) prefer `content.blurhash` for an instant local placeholder, and still fall back to `sizes['loader-square-blurred-after']`. `width` / `height` set CSS `aspect-ratio` to reduce layout shift.
|
|
19
|
+
|
|
14
20
|
## Schemas
|
|
15
21
|
|
|
16
22
|
Task outcome schemas live in `src/*.schema.js` and are auto-discovered at build time:
|
|
@@ -44,6 +50,7 @@ export async function run ({ event, sdk }) { ... }
|
|
|
44
50
|
|
|
45
51
|
## Dependencies
|
|
46
52
|
|
|
47
|
-
- **`sharp`** (peer) — image resizing in `resize-common-web`
|
|
53
|
+
- **`sharp`** (peer) — image resizing + metadata in `resize-common-web`
|
|
54
|
+
- **`blurhash`** — encode compact placeholders after resize
|
|
48
55
|
- **`openai`** (peer) — GPT-4o vision in `visual-content-descriptors`
|
|
49
56
|
- **`@ossy/platform`** — `StorageClient.load` for reading uploaded file bytes (provided at runtime by the platform server)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/media-tasks",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Changestream-triggered media processing tasks (image resize, AI descriptors)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"src": "./src"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest --verbose",
|
|
15
16
|
"typecheck": "echo \"No TypeScript in media-tasks package\" && exit 0"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"@ossy/observability": "^3.0.9",
|
|
19
|
-
"@ossy/resources": "^3.
|
|
20
|
+
"@ossy/resources": "^3.4.0",
|
|
21
|
+
"blurhash": "^2.0.5"
|
|
20
22
|
},
|
|
21
23
|
"peerDependencies": {
|
|
22
24
|
"openai": ">=4",
|
|
@@ -32,5 +34,5 @@
|
|
|
32
34
|
],
|
|
33
35
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
34
36
|
"license": "MIT",
|
|
35
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "d36b69444268d172bc3e8e1dc77e67afc63f8650"
|
|
36
38
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a BlurHash and read natural dimensions from an image buffer via sharp.
|
|
3
|
+
*
|
|
4
|
+
* @param {import('sharp')} sharp
|
|
5
|
+
* @param {Buffer} sourceBuffer
|
|
6
|
+
* @param {{ componentX?: number, componentY?: number, maxSide?: number }} [options]
|
|
7
|
+
* @returns {Promise<{ blurhash: string, width: number, height: number }>}
|
|
8
|
+
*/
|
|
9
|
+
export async function encodeBlurhashFromBuffer (sharp, sourceBuffer, options = {}) {
|
|
10
|
+
const componentX = options.componentX ?? 4
|
|
11
|
+
const componentY = options.componentY ?? 3
|
|
12
|
+
const maxSide = options.maxSide ?? 32
|
|
13
|
+
|
|
14
|
+
const metadata = await sharp(sourceBuffer).metadata()
|
|
15
|
+
const width = metadata.width
|
|
16
|
+
const height = metadata.height
|
|
17
|
+
if (!width || !height) {
|
|
18
|
+
throw new Error('encodeBlurhashFromBuffer: image metadata missing width/height')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { data, info } = await sharp(sourceBuffer)
|
|
22
|
+
.ensureAlpha()
|
|
23
|
+
.resize(maxSide, maxSide, { fit: 'inside' })
|
|
24
|
+
.raw()
|
|
25
|
+
.toBuffer({ resolveWithObject: true })
|
|
26
|
+
|
|
27
|
+
const { encode } = await import('blurhash')
|
|
28
|
+
const blurhash = encode(
|
|
29
|
+
new Uint8ClampedArray(data),
|
|
30
|
+
info.width,
|
|
31
|
+
info.height,
|
|
32
|
+
componentX,
|
|
33
|
+
componentY,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return { blurhash, width, height }
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import sharp from 'sharp'
|
|
2
|
+
import { decode } from 'blurhash'
|
|
3
|
+
import { encodeBlurhashFromBuffer } from './encode-blurhash.js'
|
|
4
|
+
|
|
5
|
+
describe('encodeBlurhashFromBuffer', () => {
|
|
6
|
+
it('encodes a blurhash and returns natural dimensions', async () => {
|
|
7
|
+
const sourceBuffer = await sharp({
|
|
8
|
+
create: {
|
|
9
|
+
width: 80,
|
|
10
|
+
height: 40,
|
|
11
|
+
channels: 3,
|
|
12
|
+
background: { r: 20, g: 120, b: 200 },
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
.png()
|
|
16
|
+
.toBuffer()
|
|
17
|
+
|
|
18
|
+
const result = await encodeBlurhashFromBuffer(sharp, sourceBuffer)
|
|
19
|
+
|
|
20
|
+
expect(result.width).toBe(80)
|
|
21
|
+
expect(result.height).toBe(40)
|
|
22
|
+
expect(typeof result.blurhash).toBe('string')
|
|
23
|
+
expect(result.blurhash.length).toBeGreaterThan(6)
|
|
24
|
+
|
|
25
|
+
const pixels = decode(result.blurhash, 4, 4)
|
|
26
|
+
expect(pixels).toBeInstanceOf(Uint8ClampedArray)
|
|
27
|
+
expect(pixels.length).toBe(4 * 4 * 4)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createLogger } from '@ossy/observability'
|
|
2
2
|
import { StorageClient } from '@ossy/platform'
|
|
3
|
-
import { GetResource } from '@ossy/resources'
|
|
3
|
+
import { GetResource, PatchResourceContent } from '@ossy/resources'
|
|
4
|
+
import { encodeBlurhashFromBuffer } from './encode-blurhash.js'
|
|
4
5
|
|
|
5
6
|
const log = createLogger('media-tasks')
|
|
6
7
|
|
|
@@ -13,6 +14,16 @@ const Sizes = [
|
|
|
13
14
|
{ width: 4, height: undefined, name: 'loader-square-blurred-after' },
|
|
14
15
|
]
|
|
15
16
|
|
|
17
|
+
/** Created fires before the client finishes PUT; wait briefly for blob bytes. */
|
|
18
|
+
async function loadSourceBuffer (key, { attempts = 20, delayMs = 250 } = {}) {
|
|
19
|
+
for (let i = 0; i < attempts; i++) {
|
|
20
|
+
const { buffer, exists } = await StorageClient.load(key)
|
|
21
|
+
if (exists && buffer) return buffer
|
|
22
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export const metadata = {
|
|
17
28
|
id: '@ossy/media-tasks/tasks/resize-common-web',
|
|
18
29
|
triggers: [
|
|
@@ -36,8 +47,8 @@ export async function run ({ event, sdk }) {
|
|
|
36
47
|
throw new Error(`[media-tasks/tasks/resize-common-web] missing storage key for ${resourceId}`)
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
50
|
+
const sourceBuffer = await loadSourceBuffer(key)
|
|
51
|
+
if (!sourceBuffer) {
|
|
41
52
|
throw new Error(`[media-tasks/tasks/resize-common-web] missing file bytes for ${resourceId}`)
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -56,4 +67,16 @@ export async function run ({ event, sdk }) {
|
|
|
56
67
|
throw error
|
|
57
68
|
}
|
|
58
69
|
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const { blurhash, width, height } = await encodeBlurhashFromBuffer(sharp, sourceBuffer)
|
|
73
|
+
log.info(`[media-tasks/tasks/resize-common-web] Patching blurhash for ${resourceId}`)
|
|
74
|
+
await sdk.invoke(PatchResourceContent, {
|
|
75
|
+
id: resourceId,
|
|
76
|
+
content: { blurhash, width, height },
|
|
77
|
+
})
|
|
78
|
+
} catch (error) {
|
|
79
|
+
log.error(`[media-tasks/tasks/resize-common-web] Error encoding blurhash for ${resourceId}`, undefined, error)
|
|
80
|
+
throw error
|
|
81
|
+
}
|
|
59
82
|
}
|