@gentbajko/slopify 0.8.4 → 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/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
|
@@ -1,61 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { entryModes, formats, stageSources } from "./model.js";
|
|
7
|
-
const providerChoice = z.object({
|
|
8
|
-
provider: z.string(),
|
|
9
|
-
model: z.string(),
|
|
10
|
-
thinking: z.enum(thinkingModes).optional(),
|
|
11
|
-
});
|
|
12
|
-
const entryChoice = z.object({ name: z.string(), mode: z.enum(entryModes) });
|
|
13
|
-
// The shape Play posts and the shape `projects.config` holds, in one place: the second
|
|
14
|
-
// is the first plus the rendered prompt texts.
|
|
15
|
-
export const runDraftSchema = z.object({
|
|
16
|
-
title: z.string(),
|
|
17
|
-
format: z.enum(formats),
|
|
18
|
-
// Spelled out rather than built from stageKinds, so the inferred type carries the six
|
|
19
|
-
// keys and this schema stays assignable to the domain type without a cast.
|
|
20
|
-
sources: z.object({
|
|
21
|
-
research: z.enum(stageSources),
|
|
22
|
-
article: z.enum(stageSources),
|
|
23
|
-
audio: z.enum(stageSources),
|
|
24
|
-
images: z.enum(stageSources),
|
|
25
|
-
thumbnail: z.enum(stageSources),
|
|
26
|
-
video: z.enum(stageSources),
|
|
27
|
-
}),
|
|
28
|
-
llm: providerChoice.optional(),
|
|
29
|
-
audio: providerChoice.extend({ voice: z.string() }).optional(),
|
|
30
|
-
images: providerChoice.optional(),
|
|
31
|
-
articlePrompt: z.string().optional(),
|
|
32
|
-
imagePrompts: z.array(z.object({ name: z.string(), number: z.number() })),
|
|
33
|
-
thumbnailPrompt: z.string().optional(),
|
|
34
|
-
intro: entryChoice.optional(),
|
|
35
|
-
outro: entryChoice.optional(),
|
|
36
|
-
values: z.record(z.string(), z.string()),
|
|
37
|
-
provided: z.object({
|
|
38
|
-
research: z.string().optional(),
|
|
39
|
-
article: z.string().optional(),
|
|
40
|
-
audio: z.string().optional(),
|
|
41
|
-
images: z.array(z.string()).optional(),
|
|
42
|
-
thumbnail: z.string().optional(),
|
|
43
|
-
}),
|
|
44
|
-
// Optional until Play carries the control; unknown keys are stripped by this schema, so
|
|
45
|
-
// a mode not listed here would never reach the audio stage.
|
|
46
|
-
chunking: z
|
|
47
|
-
.object({
|
|
48
|
-
mode: z.enum(chunkModes),
|
|
49
|
-
words: z.number().optional(),
|
|
50
|
-
characters: z.number().int().min(1).max(1000000).optional(),
|
|
51
|
-
})
|
|
52
|
-
.optional(),
|
|
53
|
-
silenceGapSeconds: z.number(),
|
|
54
|
-
subtitles: subtitleConfigSchema.optional(),
|
|
55
|
-
});
|
|
56
|
-
export const runConfigSchema = runDraftSchema.extend({
|
|
57
|
-
rendered: z.record(z.string(), z.string()),
|
|
58
|
-
});
|
|
3
|
+
import { formats, stageSources } from "./model.js";
|
|
4
|
+
import { runConfigSchema } from "./schema.js";
|
|
5
|
+
export { runConfigSchema, runDraftSchema } from "./schema.js";
|
|
59
6
|
const projectRow = z.object({
|
|
60
7
|
id: z.string(),
|
|
61
8
|
title: z.string(),
|
|
@@ -182,6 +129,8 @@ export function setProjectPaused(db, projectId, paused, at) {
|
|
|
182
129
|
db.prepare("UPDATE projects SET updated_at = ? WHERE id = ?").run(at, projectId);
|
|
183
130
|
}
|
|
184
131
|
export function updateProjectConfig(db, projectId, config, at) {
|
|
132
|
+
if (db.prepare("SELECT 1 FROM project_heads WHERE project_id=?").get(projectId) !== undefined)
|
|
133
|
+
throw new Error("Versioned project configuration must be saved through revisions.");
|
|
185
134
|
db.prepare("UPDATE projects SET config = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(config), at, projectId);
|
|
186
135
|
}
|
|
187
136
|
// A JSON column never reaches a caller as a string.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
3
|
+
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
4
|
+
import { chunkModes } from "../narration/chunk.js";
|
|
5
|
+
import { subtitleConfigSchema } from "../subtitles/model.js";
|
|
6
|
+
import { entryModes, formats, stageSources } from "./model.js";
|
|
7
|
+
const providerChoice = z.object({
|
|
8
|
+
provider: z.string(),
|
|
9
|
+
model: z.string(),
|
|
10
|
+
thinking: z.enum(thinkingModes).optional(),
|
|
11
|
+
});
|
|
12
|
+
const entryChoice = z.object({ name: z.string(), mode: z.enum(entryModes) });
|
|
13
|
+
// The shape Play posts and the shape `projects.config` holds, in one place: the second
|
|
14
|
+
// is the first plus the rendered prompt texts.
|
|
15
|
+
export const runDraftSchema = z.object({
|
|
16
|
+
checkpoints: z
|
|
17
|
+
.array(checkpointStageSchema)
|
|
18
|
+
.max(3)
|
|
19
|
+
.refine((stages) => new Set(stages).size === stages.length)
|
|
20
|
+
.readonly()
|
|
21
|
+
.optional(),
|
|
22
|
+
title: z.string(),
|
|
23
|
+
format: z.enum(formats),
|
|
24
|
+
// Spelled out rather than built from stageKinds, so the inferred type carries the six
|
|
25
|
+
// keys and this schema stays assignable to the domain type without a cast.
|
|
26
|
+
sources: z.object({
|
|
27
|
+
research: z.enum(stageSources),
|
|
28
|
+
article: z.enum(stageSources),
|
|
29
|
+
audio: z.enum(stageSources),
|
|
30
|
+
images: z.enum(stageSources),
|
|
31
|
+
thumbnail: z.enum(stageSources),
|
|
32
|
+
video: z.enum(stageSources),
|
|
33
|
+
}),
|
|
34
|
+
llm: providerChoice.optional(),
|
|
35
|
+
audio: providerChoice.extend({ voice: z.string() }).optional(),
|
|
36
|
+
images: providerChoice.optional(),
|
|
37
|
+
articlePrompt: z.string().optional(),
|
|
38
|
+
imagePrompts: z.array(z.object({ name: z.string(), number: z.number() })),
|
|
39
|
+
thumbnailPrompt: z.string().optional(),
|
|
40
|
+
intro: entryChoice.optional(),
|
|
41
|
+
outro: entryChoice.optional(),
|
|
42
|
+
values: z.record(z.string(), z.string()),
|
|
43
|
+
provided: z.object({
|
|
44
|
+
research: z.string().optional(),
|
|
45
|
+
article: z.string().optional(),
|
|
46
|
+
audio: z.string().optional(),
|
|
47
|
+
images: z.array(z.string()).optional(),
|
|
48
|
+
thumbnail: z.string().optional(),
|
|
49
|
+
}),
|
|
50
|
+
// Optional until Play carries the control; unknown keys are stripped by this schema, so
|
|
51
|
+
// a mode not listed here would never reach the audio stage.
|
|
52
|
+
chunking: z
|
|
53
|
+
.object({
|
|
54
|
+
mode: z.enum(chunkModes),
|
|
55
|
+
words: z.number().optional(),
|
|
56
|
+
characters: z.number().int().min(1).max(1000000).optional(),
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
silenceGapSeconds: z.number(),
|
|
60
|
+
subtitles: subtitleConfigSchema.optional(),
|
|
61
|
+
});
|
|
62
|
+
export const runConfigSchema = runDraftSchema.extend({
|
|
63
|
+
rendered: z.record(z.string(), z.string()),
|
|
64
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { transact } from "../../kernel/db/tx.js";
|
|
2
2
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
3
3
|
import { storeArticleText } from "../article/store.js";
|
|
4
|
-
import {
|
|
4
|
+
import { admitInitialRevision } from "../rebuild/runtime-admission.js";
|
|
5
|
+
import { adoptBaseline } from "../revisions/adopt.js";
|
|
6
|
+
import { attachStagedFile, storeText } from "../storage/staging.js";
|
|
7
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
5
8
|
import { insertProject, insertStage } from "./repo.js";
|
|
6
9
|
// Provide → `provided` with its output attached; Off → `skipped`;
|
|
7
10
|
// everything else → `pending`.
|
|
@@ -13,7 +16,7 @@ export function initialState(source) {
|
|
|
13
16
|
}
|
|
14
17
|
// The project row, its six stages and the provided content all land together or not at
|
|
15
18
|
// all. The caller ticks the runner after this returns, never inside it.
|
|
16
|
-
export function startRun(deps, draft, rendered, retainStaged = false) {
|
|
19
|
+
export function startRun(deps, draft, rendered, retainStaged = false, templates) {
|
|
17
20
|
const id = deps.ids.next();
|
|
18
21
|
const at = deps.clock.now().toISOString();
|
|
19
22
|
const config = { ...draft, rendered };
|
|
@@ -50,9 +53,15 @@ export function startRun(deps, draft, rendered, retainStaged = false) {
|
|
|
50
53
|
insertStage(deps.db, stage);
|
|
51
54
|
}
|
|
52
55
|
attachProvided(deps, id, draft, moved, retainStaged);
|
|
56
|
+
if (deps.catalogue !== undefined) {
|
|
57
|
+
const baseline = adoptBaseline(deps, id, templates);
|
|
58
|
+
if (!baseline.ok)
|
|
59
|
+
throw new Error("The new project has no revision.");
|
|
60
|
+
admitInitialRevision(deps, baseline.view, deps.catalogue.read());
|
|
61
|
+
}
|
|
53
62
|
});
|
|
54
63
|
for (const source of retainStaged ? [] : moved) {
|
|
55
|
-
|
|
64
|
+
releaseStagedFile(deps, source);
|
|
56
65
|
}
|
|
57
66
|
return { project, stages };
|
|
58
67
|
}
|
|
@@ -101,5 +110,5 @@ function attach(deps, projectId, kind, stagedFileId, role, collected, retainStag
|
|
|
101
110
|
// the file went away between the check and the write.
|
|
102
111
|
throw new Error(`the ${kind} stage's file could not be attached: ${result.reason}`);
|
|
103
112
|
}
|
|
104
|
-
collected.push(
|
|
113
|
+
collected.push(stagedFileId);
|
|
105
114
|
}
|
|
@@ -43,19 +43,22 @@ export async function writeArticle(providers, choice, brief, onDelta) {
|
|
|
43
43
|
onDelta(event.text);
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
let
|
|
46
|
+
let result = await providers.llm({
|
|
47
47
|
provider: choice.provider,
|
|
48
48
|
model: choice.model,
|
|
49
49
|
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
50
50
|
messages: base,
|
|
51
51
|
check: written,
|
|
52
52
|
}, stream);
|
|
53
|
+
if (!result.ok)
|
|
54
|
+
return result;
|
|
55
|
+
let answer = result.value;
|
|
53
56
|
const pieces = [answer.text];
|
|
54
57
|
let tokens = plusUsage(noTokens, answer.usage);
|
|
55
58
|
for (let n = 1; truncated(answer) && n <= continuationLimit; n += 1) {
|
|
56
59
|
const messages = continuationMessages(base, pieces.join(""));
|
|
57
60
|
sent.push({ label: `Continuation ${String(n)}`, messages });
|
|
58
|
-
|
|
61
|
+
result = await providers.llm({
|
|
59
62
|
provider: choice.provider,
|
|
60
63
|
model: choice.model,
|
|
61
64
|
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
@@ -65,10 +68,13 @@ export async function writeArticle(providers, choice, brief, onDelta) {
|
|
|
65
68
|
// loop therefore never sees a truncated answer with its budget spent.
|
|
66
69
|
check: n === continuationLimit ? finished : written,
|
|
67
70
|
}, stream);
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
return result;
|
|
73
|
+
answer = result.value;
|
|
68
74
|
pieces.push(answer.text);
|
|
69
75
|
tokens = plusUsage(tokens, answer.usage);
|
|
70
76
|
}
|
|
71
|
-
return { markdown: pieces.join(""), sent, tokens };
|
|
77
|
+
return { ok: true, value: { markdown: pieces.join(""), sent, tokens } };
|
|
72
78
|
}
|
|
73
79
|
function truncated(answer) {
|
|
74
80
|
return answer.finishReason === truncatedReason;
|
|
@@ -14,9 +14,9 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
|
|
|
14
14
|
throw new Error(`project ${projectId} has no row`);
|
|
15
15
|
const { config } = project;
|
|
16
16
|
if (config.sources.article !== "provide" || config.sources.audio !== "generate")
|
|
17
|
-
return;
|
|
17
|
+
return "done";
|
|
18
18
|
if (!config.intro && !config.outro)
|
|
19
|
-
return;
|
|
19
|
+
return "done";
|
|
20
20
|
audioContext.signal.throwIfAborted();
|
|
21
21
|
const article = stagesOf(deps.db, projectId).find((stage) => stage.kind === "article");
|
|
22
22
|
if (!article)
|
|
@@ -36,8 +36,11 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
|
|
|
36
36
|
if (completed.some((piece) => piece.idx === index + 1 && piece.state === "done"))
|
|
37
37
|
continue;
|
|
38
38
|
const sent = [];
|
|
39
|
-
const
|
|
39
|
+
const result = await writeSegment(providers, config.llm, config, category, narration, sent);
|
|
40
40
|
audioContext.signal.throwIfAborted();
|
|
41
|
+
if (!result.ok)
|
|
42
|
+
return "held";
|
|
43
|
+
const segment = result.value;
|
|
41
44
|
if (!segment)
|
|
42
45
|
continue;
|
|
43
46
|
if (sent.length > 0) {
|
|
@@ -59,4 +62,5 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
|
|
|
59
62
|
...segment.tokens,
|
|
60
63
|
});
|
|
61
64
|
}
|
|
65
|
+
return "done";
|
|
62
66
|
}
|
|
@@ -24,7 +24,10 @@ export async function runArticle(deps, context, providers) {
|
|
|
24
24
|
}
|
|
25
25
|
const notes = researchNotes(deps, projectId);
|
|
26
26
|
const brief = { articlePrompt, ...(notes === undefined ? {} : { notes }) };
|
|
27
|
-
const
|
|
27
|
+
const result = await articleBody(deps, context, providers, choice, brief);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
return "held";
|
|
30
|
+
const completed = result.value;
|
|
28
31
|
const { narration } = completed;
|
|
29
32
|
const sent = [...completed.sent];
|
|
30
33
|
for (const [index, category] of categories.entries()) {
|
|
@@ -33,7 +36,10 @@ export async function runArticle(deps, context, providers) {
|
|
|
33
36
|
if (completed.resumed &&
|
|
34
37
|
piecesOf(deps.db, context.stage.id, "segment").some((piece) => piece.idx === index + 1 && piece.state === "done"))
|
|
35
38
|
continue;
|
|
36
|
-
const
|
|
39
|
+
const result = await writeSegment(providers, choice, project.config, category, narration, sent);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
return "held";
|
|
42
|
+
const segment = result.value;
|
|
37
43
|
if (segment !== undefined) {
|
|
38
44
|
keepSegment(deps, context, segment, index + 1);
|
|
39
45
|
checkpointArticle(deps, context, sent);
|
|
@@ -60,6 +66,7 @@ export async function runArticle(deps, context, providers) {
|
|
|
60
66
|
stage: "article",
|
|
61
67
|
detail: `${String(narration.length)} characters to narrate`,
|
|
62
68
|
});
|
|
69
|
+
return "done";
|
|
63
70
|
}
|
|
64
71
|
// Once the article is stored, a pause during end matter resumes that end matter
|
|
65
72
|
// instead of buying and replacing the article again. Re-run clears this checkpoint.
|
|
@@ -82,14 +89,20 @@ async function articleBody(deps, context, providers, choice, brief) {
|
|
|
82
89
|
existsSync(outputPath(deps.paths, projectId, plain.path)) &&
|
|
83
90
|
existsSync(outputPath(deps.paths, projectId, markdown.path))) {
|
|
84
91
|
return {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
ok: true,
|
|
93
|
+
value: {
|
|
94
|
+
narration: readFileSync(outputPath(deps.paths, projectId, plain.path), "utf8"),
|
|
95
|
+
sent: articleCheckpoint.parse(JSON.parse(saved.payload)).sent,
|
|
96
|
+
resumed: true,
|
|
97
|
+
},
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
|
-
const
|
|
100
|
+
const result = await writeArticle(providers, choice, brief, (text) => {
|
|
91
101
|
context.emit({ type: "article.delta", projectId, text });
|
|
92
102
|
});
|
|
103
|
+
if (!result.ok)
|
|
104
|
+
return result;
|
|
105
|
+
const written = result.value;
|
|
93
106
|
storeText(deps, { projectId, stageKind: "article", role: "article_md", text: written.markdown });
|
|
94
107
|
const narration = storeArticleText(deps, { projectId, markdown: written.markdown });
|
|
95
108
|
checkpointArticle(deps, context, written.sent);
|
|
@@ -99,7 +112,7 @@ async function articleBody(deps, context, providers, choice, brief) {
|
|
|
99
112
|
model: choice.model,
|
|
100
113
|
...written.tokens,
|
|
101
114
|
});
|
|
102
|
-
return { narration, sent: written.sent, resumed: false };
|
|
115
|
+
return { ok: true, value: { narration, sent: written.sent, resumed: false } };
|
|
103
116
|
}
|
|
104
117
|
function checkpointArticle(deps, context, sent) {
|
|
105
118
|
const existing = piecesOf(deps.db, context.stage.id, "article_written")[0];
|
|
@@ -4,13 +4,13 @@ export const categories = ["intro", "outro"];
|
|
|
4
4
|
export async function writeSegment(providers, choice, config, category, article, sent) {
|
|
5
5
|
const picked = config[category];
|
|
6
6
|
if (picked === undefined)
|
|
7
|
-
return undefined;
|
|
7
|
+
return { ok: true, value: undefined };
|
|
8
8
|
const body = config.rendered[category];
|
|
9
9
|
if (body === undefined)
|
|
10
10
|
throw new Error(`the run has no rendered ${category} text`);
|
|
11
11
|
const common = { category, name: picked.name, mode: picked.mode };
|
|
12
12
|
if (picked.mode === "text")
|
|
13
|
-
return { ...common, text: body, tokens: noTokens };
|
|
13
|
+
return { ok: true, value: { ...common, text: body, tokens: noTokens } };
|
|
14
14
|
if (choice === undefined)
|
|
15
15
|
throw new Error(`the ${category} has no LLM provider or model`);
|
|
16
16
|
const messages = segmentMessages(body, config, article);
|
|
@@ -22,9 +22,18 @@ export async function writeSegment(providers, choice, config, category, article,
|
|
|
22
22
|
messages,
|
|
23
23
|
check: (given) => given.text.trim() === "" ? `the ${category} answered with nothing` : undefined,
|
|
24
24
|
});
|
|
25
|
-
|
|
25
|
+
if (!answer.ok)
|
|
26
|
+
return answer;
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
value: {
|
|
30
|
+
...common,
|
|
31
|
+
text: answer.value.text.trim(),
|
|
32
|
+
tokens: plusUsage(noTokens, answer.value.usage),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
|
-
function segmentMessages(body, config, article) {
|
|
36
|
+
export function segmentMessages(body, config, article) {
|
|
28
37
|
const values = Object.entries(config.values);
|
|
29
38
|
return [
|
|
30
39
|
{
|
|
@@ -3,9 +3,8 @@ import { transact } from "../../kernel/db/tx.js";
|
|
|
3
3
|
import { derive } from "../../kernel/runner/graph.js";
|
|
4
4
|
import { projectPaused, stagesOf } from "../admission/repo.js";
|
|
5
5
|
import { startRun } from "../admission/start.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { dropStagedSource } from "../storage/staging.js";
|
|
6
|
+
import { deleteStagedFile } from "../storage/repo.js";
|
|
7
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
9
8
|
const queueRow = z.object({
|
|
10
9
|
projectId: z.string(),
|
|
11
10
|
batchId: z.string(),
|
|
@@ -28,7 +27,7 @@ export function queueWaiting(db, projectId) {
|
|
|
28
27
|
export function batchExists(db, id) {
|
|
29
28
|
return db.prepare("SELECT 1 FROM batches WHERE id = ?").get(id) !== undefined;
|
|
30
29
|
}
|
|
31
|
-
export function enqueueBatch(deps, batchId, runs) {
|
|
30
|
+
export function enqueueBatch(deps, batchId, runs, retainStaged = false) {
|
|
32
31
|
if (batchExists(deps.db, batchId))
|
|
33
32
|
return queueEntries(deps.db, batchId);
|
|
34
33
|
const sources = new Set();
|
|
@@ -37,8 +36,8 @@ export function enqueueBatch(deps, batchId, runs) {
|
|
|
37
36
|
.prepare("INSERT INTO batches(id, created_at) VALUES (?, ?)")
|
|
38
37
|
.run(batchId, deps.clock.now().toISOString());
|
|
39
38
|
const used = new Set();
|
|
40
|
-
for (const { draft, rendered } of runs) {
|
|
41
|
-
const { project } = startRun(deps, draft, rendered, true);
|
|
39
|
+
for (const { draft, rendered, templates } of runs) {
|
|
40
|
+
const { project } = startRun(deps, draft, rendered, true, templates);
|
|
42
41
|
deps.db
|
|
43
42
|
.prepare("INSERT INTO project_queue(project_id, batch_id) VALUES (?, ?)")
|
|
44
43
|
.run(project.id, batchId);
|
|
@@ -50,15 +49,13 @@ export function enqueueBatch(deps, batchId, runs) {
|
|
|
50
49
|
for (const id of draft.provided.images ?? [])
|
|
51
50
|
used.add(id);
|
|
52
51
|
}
|
|
53
|
-
for (const id of used) {
|
|
54
|
-
|
|
55
|
-
if (file)
|
|
56
|
-
sources.add(stagingPath(deps.paths, file.path));
|
|
52
|
+
for (const id of retainStaged ? [] : used) {
|
|
53
|
+
sources.add(id);
|
|
57
54
|
deleteStagedFile(deps.db, id);
|
|
58
55
|
}
|
|
59
56
|
});
|
|
60
57
|
for (const source of sources)
|
|
61
|
-
|
|
58
|
+
releaseStagedFile(deps, source);
|
|
62
59
|
return queueEntries(deps.db, batchId);
|
|
63
60
|
}
|
|
64
61
|
// Only one batch video runs at once. A paused item holds its place; a failed or
|
|
@@ -1,84 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { projectStates, stageKinds, } from "../../kernel/pipeline.js";
|
|
1
4
|
import { derive } from "../../kernel/runner/graph.js";
|
|
2
5
|
import { finishStage, projectExists, projectPaused, setProjectPaused, stagesOf, } from "../admission/repo.js";
|
|
6
|
+
import { withProjectControl } from "../control/lock.js";
|
|
7
|
+
import { checkRevisionControl, rememberRevisionControl, } from "../control/revision-control.js";
|
|
8
|
+
import { settleScheduleRunsForProject } from "../schedules/repo.js";
|
|
3
9
|
// Cancel on the project header. Every in-flight call of this project is aborted at once, every
|
|
4
10
|
// `done` output and every finished piece is kept for the resume, and the project sits
|
|
5
11
|
// `canceled` until the user retries a stage.
|
|
6
12
|
// The cancel rules and the stage transition table, in the words the stage row carries.
|
|
7
13
|
export const canceledByUser = "canceled by user";
|
|
8
|
-
export
|
|
14
|
+
export function cancelProject(deps, projectId, input) {
|
|
15
|
+
return withProjectControl(deps.db, projectId, async () => {
|
|
16
|
+
const checked = checkRevisionControl(deps, projectId, "cancel", input, z.object({
|
|
17
|
+
ok: z.literal(true),
|
|
18
|
+
canceled: z.array(z.enum(stageKinds)),
|
|
19
|
+
state: z.enum(projectStates),
|
|
20
|
+
}));
|
|
21
|
+
if (!checked.ok)
|
|
22
|
+
return checked;
|
|
23
|
+
if (checked.response !== undefined)
|
|
24
|
+
return checked.response;
|
|
25
|
+
const result = await cancel(deps, projectId);
|
|
26
|
+
if (result.ok)
|
|
27
|
+
rememberRevisionControl(deps, checked.identity, result);
|
|
28
|
+
return result;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function cancel(deps, projectId) {
|
|
9
32
|
if (!projectExists(deps.db, projectId)) {
|
|
10
33
|
return { ok: false, reason: "no-project" };
|
|
11
34
|
}
|
|
12
35
|
const before = stagesOf(deps.db, projectId);
|
|
13
36
|
const paused = projectPaused(deps.db, projectId);
|
|
14
|
-
if (paused) {
|
|
15
|
-
setProjectPaused(deps.db, projectId, false, deps.clock.now().toISOString());
|
|
16
|
-
deps.emit(projectId, { type: "project.updated", projectId });
|
|
17
|
-
}
|
|
18
37
|
const running = before.filter((stage) => stage.state === "running").map((stage) => stage.kind);
|
|
19
|
-
|
|
38
|
+
const admittedPending = deps.db
|
|
39
|
+
.prepare("SELECT 1 FROM revision_work w JOIN revision_work_reservations r ON r.work_id=w.id JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id WHERE w.project_id=? AND w.state='pending' LIMIT 1")
|
|
40
|
+
.get(projectId) !== undefined;
|
|
41
|
+
const draining = deps.hasInflight?.(projectId) === true;
|
|
42
|
+
if (running.length === 0 && !paused && !admittedPending && !draining) {
|
|
20
43
|
// A second click is a no-op. Nothing is aborted and no state changes, so
|
|
21
44
|
// the page is simply told what the project already reads.
|
|
22
45
|
return { ok: true, canceled: [], state: derive(before) };
|
|
23
46
|
}
|
|
24
47
|
// Nothing waits for a response. The runner holds the controllers and its own
|
|
25
48
|
// barrier, so a stage that finishes during this does not release its dependents.
|
|
26
|
-
if (running.length > 0)
|
|
49
|
+
if (running.length > 0 || draining)
|
|
27
50
|
await deps.abort(projectId);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
const at = deps.clock.now().toISOString();
|
|
52
|
+
const transition = transact(deps.db, () => {
|
|
53
|
+
if (paused)
|
|
54
|
+
setProjectPaused(deps.db, projectId, false, at);
|
|
55
|
+
deps.db
|
|
56
|
+
.prepare("UPDATE revision_work SET state='canceled',dispatch_state='held',failure_reason=? WHERE project_id=? AND state!='done'")
|
|
57
|
+
.run(canceledByUser, projectId);
|
|
58
|
+
deps.db
|
|
59
|
+
.prepare("UPDATE revision_work_pieces SET state='held',dispatch_state='held' WHERE work_id IN (SELECT id FROM revision_work WHERE project_id=?) AND state!='done'")
|
|
60
|
+
.run(projectId);
|
|
61
|
+
deps.db
|
|
62
|
+
.prepare("UPDATE review_checkpoints SET state='canceled',approved_at=NULL WHERE project_id=? AND revision_id=(SELECT revision_id FROM project_heads WHERE project_id=?) AND state IN ('configured','pending-review','held')")
|
|
63
|
+
.run(projectId, projectId);
|
|
64
|
+
deps.settleCheckpoints(projectId);
|
|
65
|
+
// The invariant: after cancel commits no stage of the project is `running`.
|
|
66
|
+
// Usually the aborted runner already wrote these rows; the sweep covers a failed
|
|
67
|
+
// unwinding write and shares this transaction with schedule settlement.
|
|
68
|
+
const after = stagesOf(deps.db, projectId);
|
|
69
|
+
const swept = [];
|
|
70
|
+
for (const stage of after) {
|
|
71
|
+
if (stage.state !== "running")
|
|
72
|
+
continue;
|
|
73
|
+
finishStage(deps.db, stage.id, "canceled", canceledByUser, at);
|
|
74
|
+
swept.push(stage.kind);
|
|
35
75
|
}
|
|
76
|
+
// An abort can race a successful stage completion. Mark one remaining pending
|
|
77
|
+
// stage canceled so cancellation is explicit, rather than guessing from any
|
|
78
|
+
// completed stage (independent images can now finish before Article).
|
|
79
|
+
const stopped = stagesOf(deps.db, projectId);
|
|
80
|
+
const waiting = stopped.find((stage) => stage.state === "pending");
|
|
81
|
+
const waitingCanceled = !stopped.some((stage) => stage.state === "canceled") && waiting !== undefined;
|
|
82
|
+
if (waitingCanceled)
|
|
83
|
+
finishStage(deps.db, waiting.id, "canceled", canceledByUser, at);
|
|
84
|
+
const settled = stagesOf(deps.db, projectId);
|
|
85
|
+
const state = derive(settled);
|
|
86
|
+
settleScheduleRunsForProject(deps.db, projectId, at);
|
|
87
|
+
return { settled, state, swept, waiting: waitingCanceled ? waiting : undefined };
|
|
88
|
+
});
|
|
89
|
+
if (paused)
|
|
90
|
+
deps.emit(projectId, { type: "project.updated", projectId });
|
|
91
|
+
for (const kind of transition.swept) {
|
|
36
92
|
deps.log.write("warn", "cancel.sweep", {
|
|
37
93
|
projectId,
|
|
38
|
-
stage:
|
|
94
|
+
stage: kind,
|
|
39
95
|
detail: "the stage was still running after its calls stopped",
|
|
40
96
|
});
|
|
41
|
-
finishStage(deps.db, stage.id, "canceled", canceledByUser, deps.clock.now().toISOString());
|
|
42
97
|
deps.emit(projectId, {
|
|
43
98
|
type: "stage.state",
|
|
44
99
|
projectId,
|
|
45
|
-
stage:
|
|
100
|
+
stage: kind,
|
|
46
101
|
state: "canceled",
|
|
47
102
|
failureReason: canceledByUser,
|
|
48
103
|
});
|
|
49
104
|
}
|
|
50
|
-
|
|
51
|
-
// stage canceled so cancellation is explicit, rather than guessing from any
|
|
52
|
-
// completed stage (independent images can now finish before Article).
|
|
53
|
-
const stopped = stagesOf(deps.db, projectId);
|
|
54
|
-
const waiting = stopped.find((stage) => stage.state === "pending");
|
|
55
|
-
if (!stopped.some((stage) => stage.state === "canceled") && waiting !== undefined) {
|
|
56
|
-
finishStage(deps.db, waiting.id, "canceled", canceledByUser, deps.clock.now().toISOString());
|
|
105
|
+
if (transition.waiting !== undefined)
|
|
57
106
|
deps.emit(projectId, {
|
|
58
107
|
type: "stage.state",
|
|
59
108
|
projectId,
|
|
60
|
-
stage: waiting.kind,
|
|
109
|
+
stage: transition.waiting.kind,
|
|
61
110
|
state: "canceled",
|
|
62
111
|
failureReason: canceledByUser,
|
|
63
112
|
});
|
|
64
|
-
|
|
65
|
-
const settled = stagesOf(deps.db, projectId);
|
|
66
|
-
const state = derive(settled);
|
|
67
|
-
if (paused ||
|
|
68
|
-
after.some((stage) => stage.state === "running") ||
|
|
69
|
-
(waiting !== undefined && !stopped.some((stage) => stage.state === "canceled"))) {
|
|
113
|
+
if (paused || transition.swept.length > 0 || transition.waiting !== undefined) {
|
|
70
114
|
// Only when the sweep above moved a row: the runner already announced the state it
|
|
71
115
|
// left the project in, and repeating it would tell every open page the same thing
|
|
72
116
|
// twice.
|
|
73
|
-
deps.emit(projectId, { type: "project.state", projectId, state });
|
|
117
|
+
deps.emit(projectId, { type: "project.state", projectId, state: transition.state });
|
|
74
118
|
}
|
|
75
119
|
return {
|
|
76
120
|
ok: true,
|
|
77
121
|
// A stage whose output was stored in the same instant as the cancel stays
|
|
78
122
|
// `done`, so what was stopped is read back rather than assumed from what was running.
|
|
79
|
-
canceled: settled
|
|
123
|
+
canceled: transition.settled
|
|
80
124
|
.filter((stage) => stage.state === "canceled" && running.includes(stage.kind))
|
|
81
125
|
.map((stage) => stage.kind),
|
|
82
|
-
state,
|
|
126
|
+
state: transition.state,
|
|
83
127
|
};
|
|
84
128
|
}
|