@animastor/player 0.1.0 → 0.1.2
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/CHANGELOG.md +49 -0
- package/README.md +4 -4
- package/package.json +4 -4
- package/src/iu-media.cjs +90 -0
- package/src/player-routes.cjs +4 -0
- package/src/player-shared.cjs +2 -1
- package/src/scene-data.cjs +22 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
Stage 143a phase 2a — versioned-pointer readers in the IU-media contour
|
|
6
|
+
(B0 / N3 / N4): with `IU_IMAGE_VERSIONED` on, the published pointer is the
|
|
7
|
+
authority; with the flag off every path is byte-identical and issues zero
|
|
8
|
+
new PG calls.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `GET /iu-image` — the route answers from the three-way verdict of
|
|
13
|
+
`resolveIuImagePath`: ok → serve the versioned file (200 + ETag), absent
|
|
14
|
+
→ the legacy fallback exactly as before, missing (row present, target
|
|
15
|
+
unsafe/absent) → **404 + log** (a broken publication can never be masked
|
|
16
|
+
by stale legacy bytes), pointer-store outage → **503 + log**.
|
|
17
|
+
- `/chunk/:id` (N3) — auto-redispatch fires only when NEITHER the legacy
|
|
18
|
+
scan NOR a live pointer proves the image; a pointer-store outage blocks
|
|
19
|
+
the repair instead of silently re-dispatching.
|
|
20
|
+
- `image_ready` (N4) — the ready verdict accounts for the published
|
|
21
|
+
pointer and never reports READY on a pointer-store outage.
|
|
22
|
+
|
|
23
|
+
### API
|
|
24
|
+
|
|
25
|
+
- No breaking changes: consumers reach the pointer only through the
|
|
26
|
+
injected image facade (the package itself never requires PG); with the
|
|
27
|
+
flag OFF the behavior matches 0.1.1 byte for byte.
|
|
28
|
+
|
|
29
|
+
## 0.1.1 (2026-10-17)
|
|
30
|
+
|
|
31
|
+
BOOK-RECOVER registry gate (stage 59, released via stage 61): the scene
|
|
32
|
+
storyboard read path self-healed PG for any disk book — when no PG durations
|
|
33
|
+
existed it computed them and upserted `image_units` keyed only on disk
|
|
34
|
+
presence, re-creating rows for deleted books on every client poll.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `GET /scene/:b/:ch/:sc/storyboard` — the `image_units` PG upsert
|
|
39
|
+
(duration self-heal) now runs only when the book is still registered
|
|
40
|
+
(owns a `books` row). Durations are still computed and returned in the
|
|
41
|
+
response; only the PG WRITE is suppressed for unregistered books.
|
|
42
|
+
|
|
43
|
+
### API
|
|
44
|
+
|
|
45
|
+
- No breaking changes. The gate rides on the existing injection seam:
|
|
46
|
+
`playerRoutes(app, redis, deps)` accepts an optional
|
|
47
|
+
`deps.isBookRegistered(bookId) → Promise<boolean>` and threads it into the
|
|
48
|
+
shared contour context (`playerShared.isBookRegistered`). When the host
|
|
49
|
+
does not inject it, behavior is identical to 0.1.0 (fail-open, legacy
|
|
50
|
+
recovery). Root exports unchanged.
|
|
51
|
+
|
|
3
52
|
## 0.1.0 — 2026-09-08
|
|
4
53
|
|
|
5
54
|
### Release preparation (npm publication)
|
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ Model and the read-side artifact-naming grammar.
|
|
|
11
11
|
**Status: EXTRACTED (0.1.0).** The playback contour physically lives in this
|
|
12
12
|
package (`src/`); the host backend consumes it only through the package
|
|
13
13
|
entrypoint. See
|
|
14
|
-
[`docs/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md`](https://github.com/Animastor/animastor/blob/main/
|
|
14
|
+
[`docs/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md`](https://github.com/Animastor/animastor-docs/blob/main/architecture/PLAYER_ROUTE_SPLIT_CHECKLIST.md)
|
|
15
15
|
(physical move COMPLETE) and
|
|
16
|
-
[`docs/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md`](https://github.com/Animastor/animastor/blob/main/
|
|
16
|
+
[`docs/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md`](https://github.com/Animastor/animastor-docs/blob/main/architecture/PLAYER_PACKAGE_EXTRACTION_READINESS_AUDIT.md).
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
19
19
|
|
|
@@ -87,8 +87,8 @@ GET /api/v1/preview/:b/:ch/:sc/:iuId GET /api/v1/book/:bookId/chunks
|
|
|
87
87
|
GET /api/v1/book/:bookId/assets-state
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
Guards: [`backend/tests/architecture/player-route-split.test.js`](https://github.com/Animastor/animastor/blob/main/backend/tests/architecture/player-route-split.test.js) (P1–P9) and
|
|
91
|
-
[`backend/tests/architecture/phase6-editor-player.test.js`](https://github.com/Animastor/animastor/blob/main/backend/tests/architecture/phase6-editor-player.test.js) (T1–T7).
|
|
90
|
+
Guards: [`backend/tests/architecture/player-route-split.test.js`](https://github.com/Animastor/animastor-backend/blob/main/backend/tests/architecture/player-route-split.test.js) (P1–P9) and
|
|
91
|
+
[`backend/tests/architecture/phase6-editor-player.test.js`](https://github.com/Animastor/animastor-backend/blob/main/backend/tests/architecture/phase6-editor-player.test.js) (T1–T7).
|
|
92
92
|
|
|
93
93
|
## License
|
|
94
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animastor/player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Animastor Player — playback HTTP contour: scene media serving (Range/206/ETag/If-Range), scene data (status/storyboard/timings/waveform), IU media, playback queue and assets-state. Reads book content only through the injected playerModel (VBook boundary); every host dependency arrives as an injected port — no config, no generation, no workflows, no Redis/PG/Redis internals in the package.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"animastor",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/Animastor/animastor.git",
|
|
34
|
+
"url": "git+https://github.com/Animastor/animastor-backend.git",
|
|
35
35
|
"directory": "packages/animastor-player"
|
|
36
36
|
},
|
|
37
|
-
"homepage": "https://github.com/Animastor/animastor/tree/main/packages/animastor-player#readme",
|
|
37
|
+
"homepage": "https://github.com/Animastor/animastor-backend/tree/main/packages/animastor-player#readme",
|
|
38
38
|
"bugs": {
|
|
39
|
-
"url": "https://github.com/Animastor/animastor/issues"
|
|
39
|
+
"url": "https://github.com/Animastor/animastor-backend/issues"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@animastor/vbook-runtime": "^0.1.0"
|
package/src/iu-media.cjs
CHANGED
|
@@ -16,6 +16,31 @@ const naming = require('./artifact-naming.cjs');
|
|
|
16
16
|
module.exports = function(app, ctx, deps) {
|
|
17
17
|
const { image, state, activeScenes, placeholderAudio } = deps;
|
|
18
18
|
|
|
19
|
+
// ── Stage 143a (phase 2a) — published-pointer proof ─────────────────
|
|
20
|
+
// The legacy scans in this file run FIRST; this helper is only consulted
|
|
21
|
+
// when they came up empty. The facade is flag-gated, so flag OFF returns
|
|
22
|
+
// "absent" with ZERO PostgreSQL calls and behavior stays byte-identical.
|
|
23
|
+
//
|
|
24
|
+
// present → a live versioned pointer exists for the scene build
|
|
25
|
+
// absent → nothing published (legacy semantics apply)
|
|
26
|
+
// error → the pointer store could not be read; callers must NOT read
|
|
27
|
+
// this as "file exists" (that would block a real repair) and
|
|
28
|
+
// must NOT read it as "no publication" either (that would
|
|
29
|
+
// trigger a silent regeneration) — they leave state as-is.
|
|
30
|
+
async function versionedImageProof(bookId, chapterId, sceneId, buildId) {
|
|
31
|
+
if (!image || typeof image.resolveVersionedSceneProof !== 'function') {
|
|
32
|
+
return { status: 'absent' };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const proof = await image.resolveVersionedSceneProof(
|
|
36
|
+
ctx.outputRoot, buildId, bookId, chapterId, sceneId);
|
|
37
|
+
return proof ? { status: 'present', path: proof } : { status: 'absent' };
|
|
38
|
+
} catch (pgErr) {
|
|
39
|
+
ctx.log(`versioned image proof unavailable for ${bookId}/${chapterId}/${sceneId}: ${pgErr.message}`);
|
|
40
|
+
return { status: 'error' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
// ── Chunk status (playback readiness, auto-redispatch repair) ────────
|
|
20
45
|
app.get('/api/v1/chunk/:id', async (req, res) => {
|
|
21
46
|
try {
|
|
@@ -72,7 +97,20 @@ module.exports = function(app, ctx, deps) {
|
|
|
72
97
|
const dirFiles = fs.readdirSync(buildDir);
|
|
73
98
|
hasIuFiles = dirFiles.some(f => f.startsWith(iuPrefix) && f.endsWith('.png'));
|
|
74
99
|
} catch {}
|
|
100
|
+
// Stage 143a (phase 2a · N3): a versioned-only build keeps its
|
|
101
|
+
// IU files under {build}/.versions/**, invisible to the scan
|
|
102
|
+
// above. Only when the legacy scan is empty do we ask for the
|
|
103
|
+
// published pointer: a LIVE pointer proves the image exists
|
|
104
|
+
// (no reset, no re-dispatch). An UNREADABLE pointer store
|
|
105
|
+
// blocks the repair too — never a silent regeneration on an
|
|
106
|
+
// outage.
|
|
107
|
+
let pointerBlocksRepair = false;
|
|
75
108
|
if (!hasIuFiles) {
|
|
109
|
+
const proof = await versionedImageProof(c.book_id, c.chapter_id, c.scene_id, c.build_id);
|
|
110
|
+
if (proof.status === 'present') hasIuFiles = true;
|
|
111
|
+
else if (proof.status === 'error') pointerBlocksRepair = true;
|
|
112
|
+
}
|
|
113
|
+
if (!hasIuFiles && !pointerBlocksRepair) {
|
|
76
114
|
ctx.log(`Missing image file for ${req.params.id} — resetting state for redispatch`);
|
|
77
115
|
c.image = false;
|
|
78
116
|
await ctx.redis.set(`animastor:chunk:${req.params.id}`, JSON.stringify(c));
|
|
@@ -105,6 +143,14 @@ module.exports = function(app, ctx, deps) {
|
|
|
105
143
|
imageReady = dirFiles.some(f => f.startsWith(iuPrefix) && f.endsWith('.png'));
|
|
106
144
|
} catch {}
|
|
107
145
|
}
|
|
146
|
+
// Stage 143a (phase 2a · N4): versioned-only builds are
|
|
147
|
+
// proven by the published pointer. Flag OFF ⇒ the helper
|
|
148
|
+
// answers "absent" with zero PG calls; a pointer-store
|
|
149
|
+
// error stays NOT ready (never a false READY).
|
|
150
|
+
if (!imageReady) {
|
|
151
|
+
const proof = await versionedImageProof(c.book_id, c.chapter_id, c.scene_id, c.build_id);
|
|
152
|
+
if (proof.status === 'present') imageReady = true;
|
|
153
|
+
}
|
|
108
154
|
}
|
|
109
155
|
}
|
|
110
156
|
const videoReady = !!(c.video && fs.existsSync(videoPath));
|
|
@@ -268,6 +314,50 @@ module.exports = function(app, ctx, deps) {
|
|
|
268
314
|
try {
|
|
269
315
|
const { bookId, chapterId, sceneId, iuId } = req.params;
|
|
270
316
|
const buildId = ctx.getEffectiveBuildId(bookId, req.query.build_id, ctx.log);
|
|
317
|
+
// Stage 143a — published versioned pointer first. The resolver
|
|
318
|
+
// is a host seam on the injected image facade and returns null
|
|
319
|
+
// IMMEDIATELY while IU_IMAGE_VERSIONED is off (zero PG calls), so
|
|
320
|
+
// with the flag off this block behaves exactly like Phase 1.
|
|
321
|
+
//
|
|
322
|
+
// Phase 2a — three verdicts instead of one null:
|
|
323
|
+
// ok → serve the immutable versioned bytes (ETag below),
|
|
324
|
+
// absent → no pointer for this key → the legacy branch runs
|
|
325
|
+
// unchanged,
|
|
326
|
+
// missing → a PUBLISHED pointer whose target is unsafe/absent:
|
|
327
|
+
// 404 with a log, never a silent fallback to the
|
|
328
|
+
// legacy file (the pointer is the authority and it is
|
|
329
|
+
// broken — legacy bytes here would be stale).
|
|
330
|
+
// A pointer-store ERROR is also fail-closed: 503 + log, no legacy
|
|
331
|
+
// fallback, because an outage must not serve a superseded image.
|
|
332
|
+
let published = null;
|
|
333
|
+
if (image && typeof image.resolveIuImagePath === 'function') {
|
|
334
|
+
try {
|
|
335
|
+
published = await image.resolveIuImagePath(ctx.outputRoot, buildId, bookId, chapterId, sceneId, iuId);
|
|
336
|
+
} catch (pgErr) {
|
|
337
|
+
ctx.log(`/iu-image pointer lookup failed for ${bookId}/${chapterId}/${sceneId}/${iuId}: ${pgErr.message}`);
|
|
338
|
+
res.status(503).json({ error: 'image pointer store unavailable' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (published && published.status === 'ok') {
|
|
343
|
+
let stat = null;
|
|
344
|
+
try { stat = fs.statSync(published.path); } catch (_) { stat = null; }
|
|
345
|
+
if (stat && stat.isFile()) {
|
|
346
|
+
res.setHeader('Content-Type', 'image/png');
|
|
347
|
+
res.setHeader('ETag', `W/"${published.fence}-${stat.size}-${stat.mtimeMs}"`);
|
|
348
|
+
fs.createReadStream(published.path).pipe(res);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
ctx.log(`/iu-image published target vanished (${published.relativePath}) — 404`);
|
|
352
|
+
res.status(404).json({ error: 'IU image not found' });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (published && published.status === 'missing') {
|
|
356
|
+
ctx.log(`/iu-image published target unusable (${published.relativePath}: ${published.reason}) — 404`);
|
|
357
|
+
res.status(404).json({ error: 'IU image not found' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// null (flag OFF) or {status:'absent'} → legacy branch, unchanged.
|
|
271
361
|
const imagePath = path.join(ctx.buildDir(buildId), naming.iuImageName(bookId, chapterId, sceneId, iuId));
|
|
272
362
|
if (!fs.existsSync(imagePath)) return res.status(404).json({ error: 'IU image not found' });
|
|
273
363
|
res.setHeader('Content-Type', 'image/png');
|
package/src/player-routes.cjs
CHANGED
|
@@ -51,6 +51,9 @@ module.exports = function(app, redis, deps) {
|
|
|
51
51
|
getChunk, getAllChunks, getBookWindowStatus,
|
|
52
52
|
activeScenes, placeholderAudio, layerConfig, iuRepo,
|
|
53
53
|
sceneAssetsRepo, playerModel,
|
|
54
|
+
// Book-registry gate (read-time self-heal must not persist for a
|
|
55
|
+
// deleted/unregistered book):
|
|
56
|
+
isBookRegistered,
|
|
54
57
|
// Player-route seams (composition root provides them):
|
|
55
58
|
outputRoot, // = config.OUTPUT_DIR (injected)
|
|
56
59
|
playerPorts, // { assertBookAccess, computeVideoStartMs, computeWaveform }
|
|
@@ -73,6 +76,7 @@ module.exports = function(app, redis, deps) {
|
|
|
73
76
|
redis,
|
|
74
77
|
getChunk,
|
|
75
78
|
iuRepo,
|
|
79
|
+
isBookRegistered,
|
|
76
80
|
log,
|
|
77
81
|
});
|
|
78
82
|
|
package/src/player-shared.cjs
CHANGED
|
@@ -37,7 +37,7 @@ const naming = require('./artifact-naming.cjs');
|
|
|
37
37
|
* - playerPorts: { assertBookAccess, computeVideoStartMs, computeWaveform }
|
|
38
38
|
*/
|
|
39
39
|
function createPlayerShared(deps) {
|
|
40
|
-
const { playerModel, playerPorts, redis, getChunk, iuRepo, log } = deps;
|
|
40
|
+
const { playerModel, playerPorts, redis, getChunk, iuRepo, isBookRegistered, log } = deps;
|
|
41
41
|
const outputRoot = deps.outputRoot;
|
|
42
42
|
const ports = playerPorts || {};
|
|
43
43
|
|
|
@@ -222,6 +222,7 @@ function createPlayerShared(deps) {
|
|
|
222
222
|
get redis() { return redis; },
|
|
223
223
|
get getChunk() { return getChunk; },
|
|
224
224
|
get iuRepo() { return iuRepo; },
|
|
225
|
+
get isBookRegistered() { return isBookRegistered; },
|
|
225
226
|
get outputRoot() { return outputRoot; },
|
|
226
227
|
get log() { return log; },
|
|
227
228
|
};
|
package/src/scene-data.cjs
CHANGED
|
@@ -157,18 +157,28 @@ module.exports = function(app, ctx, deps) {
|
|
|
157
157
|
iu._end_ms = endMs;
|
|
158
158
|
cursorMs = endMs;
|
|
159
159
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
// Registration gate: only persist the duration self-heal for a
|
|
161
|
+
// book that is still in the registry. A deleted book whose disk
|
|
162
|
+
// book.json remains would otherwise get its image_units rows
|
|
163
|
+
// re-created here every time a client polls the scene storyboard.
|
|
164
|
+
// The in-memory durations are still computed and returned; only
|
|
165
|
+
// the PG write is suppressed.
|
|
166
|
+
const registered = typeof ctx.isBookRegistered !== 'function'
|
|
167
|
+
? true : await ctx.isBookRegistered(bookId);
|
|
168
|
+
if (registered) {
|
|
169
|
+
for (const [idx, iu] of ius.entries()) {
|
|
170
|
+
try {
|
|
171
|
+
await ctx.iuRepo.upsertImageUnit(buildId, bookId, chapterId, sceneId, iu.unit_id, {
|
|
172
|
+
scene_order: idx, text: iu.text, text_length: (iu.text || '').length,
|
|
173
|
+
text_proportion: iu.text_proportion || 0, scene_duration_sec: sceneDuration || 0,
|
|
174
|
+
estimated_duration_sec: iu.estimated_duration_sec || 0,
|
|
175
|
+
scene_audio_file: naming.sceneAudioName(bookId, chapterId, sceneId),
|
|
176
|
+
start_ms: iu._start_ms != null ? iu._start_ms : null,
|
|
177
|
+
end_ms: iu._end_ms != null ? iu._end_ms : null,
|
|
178
|
+
});
|
|
179
|
+
} catch (pgErr) {
|
|
180
|
+
console.warn('[SCENE STORYBOARD] Failed to persist IU to PG:', pgErr.message);
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
}
|