@gentbajko/slopify 0.8.3 → 1.0.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 +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
2
|
+
import { setProjectPaused } from "../admission/repo.js";
|
|
3
|
+
import { requestHash as hash } from "../revisions/mutation-request.js";
|
|
4
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
5
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
6
|
+
import { rebuildAdmissionSchema, } from "./model.js";
|
|
7
|
+
import { executionSnapshotSchema, planPreview, reviewStillCovers } from "./preview-plan.js";
|
|
8
|
+
import { previewById } from "./repo.js";
|
|
9
|
+
import { insertInvocation } from "./runtime-admission.js";
|
|
10
|
+
import { bindNarrationReuse } from "./runtime-narration-reuse.js";
|
|
11
|
+
import { projectStandings } from "./runtime-store.js";
|
|
12
|
+
export function admissionReceipt(db, projectId, idempotencyKey, requestHash) {
|
|
13
|
+
const row = db
|
|
14
|
+
.prepare("SELECT request_hash,response_json FROM rebuild_admissions WHERE project_id=? AND idempotency_key=?")
|
|
15
|
+
.get(projectId, idempotencyKey);
|
|
16
|
+
if (row !== undefined)
|
|
17
|
+
return row.request_hash !== requestHash
|
|
18
|
+
? { ok: false, reason: "conflict" }
|
|
19
|
+
: {
|
|
20
|
+
ok: true,
|
|
21
|
+
value: {
|
|
22
|
+
...rebuildAdmissionSchema.parse(JSON.parse(String(row.response_json))),
|
|
23
|
+
replayed: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
if (db
|
|
27
|
+
.prepare("SELECT 1 FROM revision_mutations WHERE project_id=? AND idempotency_key=? UNION ALL SELECT 1 FROM project_control_receipts WHERE project_id=? AND idempotency_key=?")
|
|
28
|
+
.get(projectId, idempotencyKey, projectId, idempotencyKey) !== undefined)
|
|
29
|
+
return { ok: false, reason: "conflict" };
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
export function admitPreview(deps, input) {
|
|
33
|
+
return transact(deps.db, () => {
|
|
34
|
+
const { preview } = input;
|
|
35
|
+
const replay = admissionReceipt(deps.db, preview.projectId, input.idempotencyKey, input.requestHash);
|
|
36
|
+
if (replay !== undefined)
|
|
37
|
+
return replay;
|
|
38
|
+
if (currentRevisionId(deps.db, preview.projectId) !== preview.baseRevisionId)
|
|
39
|
+
return { ok: false, reason: "conflict" };
|
|
40
|
+
const persisted = previewById(deps.db, preview.projectId, preview.id);
|
|
41
|
+
const row = deps.db
|
|
42
|
+
.prepare("SELECT execution_json FROM rebuild_previews WHERE id=? AND project_id=?")
|
|
43
|
+
.get(preview.id, preview.projectId);
|
|
44
|
+
if (persisted === undefined ||
|
|
45
|
+
typeof row?.execution_json !== "string" ||
|
|
46
|
+
hash("body", "", persisted) !== hash("body", "", preview))
|
|
47
|
+
return { ok: false, reason: "stale-preview" };
|
|
48
|
+
const snapshot = executionSnapshotSchema.parse(JSON.parse(row.execution_json));
|
|
49
|
+
const view = getRevisionView(deps, preview.projectId, preview.baseRevisionId);
|
|
50
|
+
if (view === undefined)
|
|
51
|
+
return { ok: false, reason: "no-project" };
|
|
52
|
+
const fresh = planPreview(deps, view, snapshot.catalogue, preview.selection, preview.id);
|
|
53
|
+
if (!fresh.ok || !reviewStillCovers(preview, snapshot, fresh.value))
|
|
54
|
+
return { ok: false, reason: "stale-preview" };
|
|
55
|
+
const admissionId = deps.ids.next();
|
|
56
|
+
const workIds = [];
|
|
57
|
+
for (const recipe of snapshot.recipes) {
|
|
58
|
+
const disposition = snapshot.dispositions.find((row) => row.key === recipe.key)?.disposition;
|
|
59
|
+
const existing = deps.db
|
|
60
|
+
.prepare(`SELECT w.id,w.state,w.recipe_context,r.piece_id,r.fingerprint FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id WHERE r.revision_id=? AND r.work_key=?`)
|
|
61
|
+
.get(view.revision.id, recipe.key);
|
|
62
|
+
if (disposition === "reuse") {
|
|
63
|
+
bindNarrationReuse(deps, view, recipe, snapshot.ordinals[recipe.key] ?? 0);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (existing?.fingerprint === recipe.fingerprint &&
|
|
67
|
+
typeof existing.recipe_context === "string" &&
|
|
68
|
+
existing.state !== "done") {
|
|
69
|
+
workIds.push(String(existing.id));
|
|
70
|
+
if (existing.state !== "running") {
|
|
71
|
+
deps.db
|
|
72
|
+
.prepare("UPDATE revision_work SET state='pending',dispatch_state='allowed',failure_reason=NULL WHERE id=?")
|
|
73
|
+
.run(String(existing.id));
|
|
74
|
+
deps.db
|
|
75
|
+
.prepare(`UPDATE revision_work_pieces SET state='pending',dispatch_state='allowed'
|
|
76
|
+
WHERE work_id=? AND state!='done' AND id IN (
|
|
77
|
+
SELECT p.id FROM revision_work_pieces p
|
|
78
|
+
JOIN revision_work_reservations r ON r.piece_id=p.id AND r.work_id=p.work_id
|
|
79
|
+
AND r.work_key=p.work_key AND r.fingerprint=p.fingerprint
|
|
80
|
+
JOIN project_revisions v ON v.id=r.revision_id
|
|
81
|
+
JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key)
|
|
82
|
+
AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint)
|
|
83
|
+
WHERE r.revision_id=? AND (p.id=? OR
|
|
84
|
+
(?='article:body' AND p.work_key LIKE 'article:continuation:%'
|
|
85
|
+
AND COALESCE(r.logical_key,r.work_key)='article:body'))
|
|
86
|
+
)`)
|
|
87
|
+
.run(String(existing.id), view.revision.id, String(existing.piece_id), recipe.key);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (existing?.state === "running")
|
|
92
|
+
throw new Error("Selected work has a mismatched running reservation.");
|
|
93
|
+
deps.db
|
|
94
|
+
.prepare("DELETE FROM revision_work_reservations WHERE revision_id=? AND work_key=?")
|
|
95
|
+
.run(view.revision.id, recipe.key);
|
|
96
|
+
const key = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
|
|
97
|
+
const fingerprint = snapshot.anchors[key];
|
|
98
|
+
if (fingerprint === undefined)
|
|
99
|
+
throw new Error("Selected work has no reviewed desired anchor.");
|
|
100
|
+
const work = insertInvocation(deps, view, recipe, snapshot.catalogue, { key, fingerprint }, false, view.revision.id, admissionId);
|
|
101
|
+
workIds.push(work.workId);
|
|
102
|
+
}
|
|
103
|
+
const receipt = {
|
|
104
|
+
revisionId: view.revision.id,
|
|
105
|
+
admissionId,
|
|
106
|
+
workIds: [...new Set(workIds)],
|
|
107
|
+
replayed: false,
|
|
108
|
+
};
|
|
109
|
+
deps.db
|
|
110
|
+
.prepare("INSERT INTO rebuild_admissions(id,project_id,revision_id,idempotency_key,request_hash,preview_id,response_json,created_at) VALUES (?,?,?,?,?,?,?,?)")
|
|
111
|
+
.run(admissionId, preview.projectId, view.revision.id, input.idempotencyKey, input.requestHash, preview.id, JSON.stringify(receipt), deps.clock.now().toISOString());
|
|
112
|
+
for (const review of snapshot.reviews)
|
|
113
|
+
deps.db
|
|
114
|
+
.prepare("INSERT INTO revision_provided_reviews(project_id,revision_id,work_key,dependency_fingerprint,admission_id) VALUES (?,?,?,?,?) ON CONFLICT(revision_id,work_key) DO UPDATE SET dependency_fingerprint=excluded.dependency_fingerprint,admission_id=excluded.admission_id")
|
|
115
|
+
.run(preview.projectId, view.revision.id, review.key, review.fingerprint, admissionId);
|
|
116
|
+
setProjectPaused(deps.db, preview.projectId, false, deps.clock.now().toISOString());
|
|
117
|
+
projectStandings(deps, preview.projectId);
|
|
118
|
+
return { ok: true, value: receipt };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function planDependencies(desired, retained) {
|
|
2
|
+
return desired.map((recipe) => {
|
|
3
|
+
const old = retained.find((one) => one.key === recipe.key &&
|
|
4
|
+
one.fingerprint === recipe.fingerprint &&
|
|
5
|
+
(one.available || one.inflight));
|
|
6
|
+
const changedProvided = recipe.kind === "provided" &&
|
|
7
|
+
retained.some((one) => one.key === recipe.key && one.fingerprint !== recipe.fingerprint);
|
|
8
|
+
const disposition = recipe.unresolved
|
|
9
|
+
? "blocked"
|
|
10
|
+
: old
|
|
11
|
+
? "reuse"
|
|
12
|
+
: changedProvided
|
|
13
|
+
? "review"
|
|
14
|
+
: recipe.kind === "provided"
|
|
15
|
+
? "blocked"
|
|
16
|
+
: recipe.kind === "local"
|
|
17
|
+
? "local"
|
|
18
|
+
: "generate";
|
|
19
|
+
return {
|
|
20
|
+
...recipe,
|
|
21
|
+
disposition,
|
|
22
|
+
inflight: old?.inflight ?? false,
|
|
23
|
+
pieceIds: old?.pieceIds ?? [],
|
|
24
|
+
reason: disposition === "reuse"
|
|
25
|
+
? "Inputs match retained work."
|
|
26
|
+
: disposition === "review"
|
|
27
|
+
? "Confirm the provided content against changed inputs."
|
|
28
|
+
: disposition === "blocked"
|
|
29
|
+
? "An input is unavailable or needs a fresh preview."
|
|
30
|
+
: "Inputs changed or a required output is missing.",
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { stageKinds } from "../../kernel/pipeline.js";
|
|
3
|
+
export const rebuildSelectionSchema = z.discriminatedUnion("kind", [
|
|
4
|
+
z.object({ kind: z.literal("allAffected") }).strict(),
|
|
5
|
+
z
|
|
6
|
+
.object({
|
|
7
|
+
kind: z.literal("selected"),
|
|
8
|
+
workKeys: z.array(z.string().min(1).max(200)).min(1).max(10000),
|
|
9
|
+
})
|
|
10
|
+
.strict(),
|
|
11
|
+
]);
|
|
12
|
+
const revisionId = z.string().min(1).max(64);
|
|
13
|
+
export const previewRebuildSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
baseRevisionId: revisionId,
|
|
16
|
+
request: rebuildSelectionSchema,
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
export const startRebuildSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
baseRevisionId: revisionId,
|
|
22
|
+
idempotencyKey: z.uuid(),
|
|
23
|
+
previewId: z.string().min(1).max(64),
|
|
24
|
+
acknowledgeUnknownCosts: z.boolean(),
|
|
25
|
+
confirmedProvidedWorkKeys: z.array(z.string().min(1).max(200)).max(10000),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
const costRowSchema = z.object({
|
|
29
|
+
stage: z.string(),
|
|
30
|
+
low: z.number().finite().nonnegative().nullable(),
|
|
31
|
+
high: z.number().finite().nonnegative().nullable(),
|
|
32
|
+
detail: z.string(),
|
|
33
|
+
});
|
|
34
|
+
const costEstimateSchema = z.object({
|
|
35
|
+
currency: z.literal("USD"),
|
|
36
|
+
rows: z.array(costRowSchema),
|
|
37
|
+
low: z.number().finite().nonnegative(),
|
|
38
|
+
high: z.number().finite().nonnegative(),
|
|
39
|
+
unknown: z.number().int().nonnegative(),
|
|
40
|
+
expectedWords: z.number().finite().nonnegative(),
|
|
41
|
+
catalogueDate: z.string().nullable(),
|
|
42
|
+
assumptions: z.array(z.string()),
|
|
43
|
+
});
|
|
44
|
+
export const rebuildAdmissionSchema = z.object({
|
|
45
|
+
revisionId,
|
|
46
|
+
admissionId: z.string().min(1).max(64),
|
|
47
|
+
workIds: z.array(z.string().min(1).max(64)),
|
|
48
|
+
replayed: z.boolean(),
|
|
49
|
+
});
|
|
50
|
+
export const rebuildPreviewSchema = z.object({
|
|
51
|
+
id: z.string().min(1).max(64),
|
|
52
|
+
projectId: z.string().min(1).max(64),
|
|
53
|
+
baseRevisionId: revisionId,
|
|
54
|
+
planFingerprint: z.string().min(1),
|
|
55
|
+
selection: rebuildSelectionSchema,
|
|
56
|
+
changedInputs: z.array(z.object({ path: z.string(), before: z.string().nullable(), after: z.string().nullable() })),
|
|
57
|
+
work: z.array(z.object({
|
|
58
|
+
key: z.string(),
|
|
59
|
+
stage: z.enum(stageKinds),
|
|
60
|
+
kind: z.enum(["provider", "local", "provided"]),
|
|
61
|
+
disposition: z.enum(["reuse", "generate", "local", "review", "blocked"]),
|
|
62
|
+
requestFingerprint: z.string(),
|
|
63
|
+
fingerprint: z.string(),
|
|
64
|
+
dependsOn: z.array(z.string()),
|
|
65
|
+
reason: z.string(),
|
|
66
|
+
inflight: z.boolean(),
|
|
67
|
+
pieceIds: z.array(z.string()),
|
|
68
|
+
})),
|
|
69
|
+
retained: z.array(z.object({
|
|
70
|
+
slot: z.string(),
|
|
71
|
+
outputId: z.string(),
|
|
72
|
+
assetId: z.string(),
|
|
73
|
+
state: z.enum(["ready", "outdated", "review"]),
|
|
74
|
+
})),
|
|
75
|
+
providedReuseRequired: z.array(z.string()),
|
|
76
|
+
costs: costEstimateSchema,
|
|
77
|
+
wholeRequestNotice: z.string().nullable(),
|
|
78
|
+
warnings: z.array(z.string()),
|
|
79
|
+
review: z
|
|
80
|
+
.object({
|
|
81
|
+
inputChanges: z.array(z.object({
|
|
82
|
+
label: z.string(),
|
|
83
|
+
before: z.string().nullable(),
|
|
84
|
+
after: z.string().nullable(),
|
|
85
|
+
})),
|
|
86
|
+
requests: z.array(z.object({
|
|
87
|
+
key: z.string(),
|
|
88
|
+
label: z.string(),
|
|
89
|
+
text: z.string().nullable(),
|
|
90
|
+
settings: z.string().nullable(),
|
|
91
|
+
})),
|
|
92
|
+
})
|
|
93
|
+
.optional(),
|
|
94
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { stagePieceSchema } from "../revisions/schema.js";
|
|
4
|
+
import { outputPath } from "../storage/layout.js";
|
|
5
|
+
export function retainedNarrationPieces(deps, projectId) {
|
|
6
|
+
return deps.db
|
|
7
|
+
.prepare(`SELECT p.piece_key,p.asset_id,p.fingerprint,p.descriptor,a.path FROM revision_pieces p
|
|
8
|
+
JOIN project_assets a ON a.id=p.asset_id AND a.project_id=p.project_id
|
|
9
|
+
WHERE p.project_id=? AND p.stage_kind='audio' ORDER BY p.created_at,p.id`)
|
|
10
|
+
.all(projectId)
|
|
11
|
+
.map((value) => {
|
|
12
|
+
const row = z
|
|
13
|
+
.object({
|
|
14
|
+
piece_key: z.string(),
|
|
15
|
+
asset_id: z.string(),
|
|
16
|
+
fingerprint: z.string(),
|
|
17
|
+
descriptor: z.string(),
|
|
18
|
+
path: z.string(),
|
|
19
|
+
})
|
|
20
|
+
.parse(value);
|
|
21
|
+
return {
|
|
22
|
+
key: row.piece_key,
|
|
23
|
+
stageKind: "audio",
|
|
24
|
+
assetId: row.asset_id,
|
|
25
|
+
fingerprint: row.fingerprint,
|
|
26
|
+
piece: stagePieceSchema.parse(JSON.parse(row.descriptor)),
|
|
27
|
+
available: statSync(outputPath(deps.paths, projectId, row.path), {
|
|
28
|
+
throwIfNoEntry: false,
|
|
29
|
+
})?.isFile() === true,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function referencedAssetsAvailable(deps, projectId, ids) {
|
|
34
|
+
return ids.filter((id) => {
|
|
35
|
+
const row = deps.db
|
|
36
|
+
.prepare("SELECT path FROM project_assets WHERE project_id=? AND id=?")
|
|
37
|
+
.get(projectId, id);
|
|
38
|
+
return (row !== undefined &&
|
|
39
|
+
statSync(outputPath(deps.paths, projectId, z.string().parse(row.path)), {
|
|
40
|
+
throwIfNoEntry: false,
|
|
41
|
+
})?.isFile() === true);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
export function matchingNarrationPiece(recipe, pieces, available) {
|
|
4
|
+
if (recipe.input.kind !== "tts")
|
|
5
|
+
return undefined;
|
|
6
|
+
const noRegeneration = recipe.fingerprint === fingerprint([recipe.requestFingerprint, null]);
|
|
7
|
+
return pieces.find((piece) => {
|
|
8
|
+
if (piece.stageKind !== "audio" ||
|
|
9
|
+
piece.piece.state !== "done" ||
|
|
10
|
+
piece.assetId === null ||
|
|
11
|
+
!available.has(piece.assetId))
|
|
12
|
+
return false;
|
|
13
|
+
const parsed = z
|
|
14
|
+
.object({ requestFingerprint: z.string() })
|
|
15
|
+
.safeParse(JSON.parse(piece.piece.payload ?? "null"));
|
|
16
|
+
return (parsed.success &&
|
|
17
|
+
parsed.data.requestFingerprint === recipe.requestFingerprint &&
|
|
18
|
+
(noRegeneration || piece.fingerprint === recipe.fingerprint));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { stageKinds } from "../../kernel/pipeline.js";
|
|
2
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
3
|
+
export function previewDetails(deps, view, allRecipes, selected) {
|
|
4
|
+
const parent = view.revision.parentId === null
|
|
5
|
+
? undefined
|
|
6
|
+
: getRevisionView(deps, view.revision.projectId, view.revision.parentId);
|
|
7
|
+
const requests = allRecipes.flatMap((row) => {
|
|
8
|
+
if (!selected.has(row.key))
|
|
9
|
+
return [];
|
|
10
|
+
const input = row.input;
|
|
11
|
+
const narration = narrationIdentity(row);
|
|
12
|
+
if (narration !== undefined) {
|
|
13
|
+
const groups = [
|
|
14
|
+
...new Set(allRecipes.flatMap((one) => {
|
|
15
|
+
const identity = narrationIdentity(one);
|
|
16
|
+
return identity?.segment === narration.segment ? [identity.logicalKey] : [];
|
|
17
|
+
})),
|
|
18
|
+
];
|
|
19
|
+
const siblings = allRecipes.filter((one) => narrationIdentity(one)?.logicalKey === narration.logicalKey);
|
|
20
|
+
const segment = narration.segment === "body" ? "Body" : narration.segment === "intro" ? "Intro" : "Outro";
|
|
21
|
+
const text = input.kind === "tts"
|
|
22
|
+
? input.text
|
|
23
|
+
: input.kind === "provided" &&
|
|
24
|
+
Array.isArray(input.semantic) &&
|
|
25
|
+
typeof input.semantic[0] === "string"
|
|
26
|
+
? input.semantic[0]
|
|
27
|
+
: null;
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
key: row.key,
|
|
31
|
+
label: `${segment} narration chunk ${groups.indexOf(narration.logicalKey) + 1} · request ${siblings.findIndex((one) => one.key === row.key) + 1}`,
|
|
32
|
+
text,
|
|
33
|
+
settings: input.kind === "tts"
|
|
34
|
+
? [input.provider, input.model, input.voice].join(" · ")
|
|
35
|
+
: "Provided audio",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
if (input.kind === "image") {
|
|
40
|
+
const index = view.revision.content.imageOrder.indexOf(row.key.slice("image:".length));
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
key: row.key,
|
|
44
|
+
label: index < 0 ? "Thumbnail image" : `Image ${index + 1}`,
|
|
45
|
+
text: input.prompt,
|
|
46
|
+
settings: [input.provider, input.model, input.aspect].join(" · "),
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
if (input.kind === "llm") {
|
|
51
|
+
const peers = allRecipes.filter((one) => one.stage === row.stage && one.input.kind === "llm");
|
|
52
|
+
const category = row.key.startsWith("entry:intro")
|
|
53
|
+
? "Intro text"
|
|
54
|
+
: row.key.startsWith("entry:outro")
|
|
55
|
+
? "Outro text"
|
|
56
|
+
: row.stage === "article"
|
|
57
|
+
? "Article"
|
|
58
|
+
: row.stage === "research"
|
|
59
|
+
? "Research"
|
|
60
|
+
: "Thumbnail prompt";
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
key: row.key,
|
|
64
|
+
label: `${category} request ${peers.findIndex((one) => one.key === row.key) + 1}`,
|
|
65
|
+
text: input.messages
|
|
66
|
+
.map((message) => `${message.role}:\n${message.content}`)
|
|
67
|
+
.join("\n\n"),
|
|
68
|
+
settings: [input.provider, input.model, input.thinking]
|
|
69
|
+
.filter((value) => value !== null)
|
|
70
|
+
.join(" · "),
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
});
|
|
76
|
+
return { inputChanges: parent === undefined ? [] : inputChanges(parent, view), requests };
|
|
77
|
+
}
|
|
78
|
+
function narrationIdentity(row) {
|
|
79
|
+
if (row.input.kind === "tts")
|
|
80
|
+
return { logicalKey: row.input.logicalKey, segment: row.input.segment };
|
|
81
|
+
if (row.input.kind !== "provided")
|
|
82
|
+
return undefined;
|
|
83
|
+
const match = /^audio:(body:.+|intro|outro):[0-9]+$/.exec(row.key);
|
|
84
|
+
if (match === null)
|
|
85
|
+
return undefined;
|
|
86
|
+
return {
|
|
87
|
+
logicalKey: row.key.slice(0, row.key.lastIndexOf(":")),
|
|
88
|
+
segment: row.key.startsWith("audio:body:")
|
|
89
|
+
? "body"
|
|
90
|
+
: row.key.startsWith("audio:intro:")
|
|
91
|
+
? "intro"
|
|
92
|
+
: "outro",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function inputChanges(parent, view) {
|
|
96
|
+
const before = parent.revision.config;
|
|
97
|
+
const after = view.revision.config;
|
|
98
|
+
const old = parent.revision.content;
|
|
99
|
+
const next = view.revision.content;
|
|
100
|
+
const changes = [];
|
|
101
|
+
const add = (label, previous, current) => {
|
|
102
|
+
const text = (value) => value == null ? null : typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
103
|
+
const a = text(previous);
|
|
104
|
+
const b = text(current);
|
|
105
|
+
if (a !== b)
|
|
106
|
+
changes.push({ label, before: a, after: b });
|
|
107
|
+
};
|
|
108
|
+
add("Project title", before.title, after.title);
|
|
109
|
+
add("Aspect ratio", before.format, after.format);
|
|
110
|
+
for (const kind of stageKinds)
|
|
111
|
+
add(`${kind[0]?.toUpperCase()}${kind.slice(1)} source`, before.sources[kind], after.sources[kind]);
|
|
112
|
+
for (const [name, a, b] of [
|
|
113
|
+
["Text", before.llm, after.llm],
|
|
114
|
+
["Narration", before.audio, after.audio],
|
|
115
|
+
["Images", before.images, after.images],
|
|
116
|
+
]) {
|
|
117
|
+
add(`${name} provider`, a?.provider, b?.provider);
|
|
118
|
+
add(`${name} model`, a?.model, b?.model);
|
|
119
|
+
add(`${name} thinking`, a?.thinking, b?.thinking);
|
|
120
|
+
}
|
|
121
|
+
add("Narration voice", before.audio?.voice, after.audio?.voice);
|
|
122
|
+
add("Narration chunking", before.chunking, after.chunking);
|
|
123
|
+
add("Silence gap (seconds)", before.silenceGapSeconds, after.silenceGapSeconds);
|
|
124
|
+
add("Intro", before.intro, after.intro);
|
|
125
|
+
add("Outro", before.outro, after.outro);
|
|
126
|
+
add("Subtitle settings", before.subtitles, after.subtitles);
|
|
127
|
+
add("Article text", parent.articleMarkdown, view.articleMarkdown);
|
|
128
|
+
add("Provided research", before.provided.research, after.provided.research);
|
|
129
|
+
for (const key of new Set([...Object.keys(before.values), ...Object.keys(after.values)]))
|
|
130
|
+
add(`Keyword: ${key}`, before.values[key], after.values[key]);
|
|
131
|
+
for (const key of new Set([...Object.keys(before.rendered), ...Object.keys(after.rendered)]))
|
|
132
|
+
add(`Prompt: ${key}`, before.rendered[key], after.rendered[key]);
|
|
133
|
+
if (old.imageOrder.join("\0") !== next.imageOrder.join("\0")) {
|
|
134
|
+
const label = (key) => old.imageOrder.includes(key) ? `Image ${old.imageOrder.indexOf(key) + 1}` : "New image";
|
|
135
|
+
add("Image order", old.imageOrder.map(label).join(", "), next.imageOrder.map(label).join(", "));
|
|
136
|
+
}
|
|
137
|
+
for (const [index, key] of next.imageOrder.entries()) {
|
|
138
|
+
add(`Image ${index + 1} prompt`, old.imageDefinitions[key]?.prompt, next.imageDefinitions[key]?.prompt);
|
|
139
|
+
if (old.imageDefinitions[key]?.assetId !== next.imageDefinitions[key]?.assetId)
|
|
140
|
+
add(`Image ${index + 1} file`, old.imageDefinitions[key]?.assetId == null ? null : "Previous file", next.imageDefinitions[key]?.assetId == null ? null : "Replacement file");
|
|
141
|
+
}
|
|
142
|
+
add("Manual captions", old.subtitleCues?.cues, next.subtitleCues?.cues);
|
|
143
|
+
return changes;
|
|
144
|
+
}
|