@gentbajko/slopify 1.2.1 → 1.3.1
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 +3 -2
- package/dist/adapter-registry.js +3 -1
- package/dist/adapters/image/codex.js +230 -0
- package/dist/catalog/validate.js +3 -1
- package/dist/kernel/db/migrations/0011-narration-prompts.sql +13 -0
- package/dist/slices/admission/rules.js +17 -1
- package/dist/slices/admission/schema.js +1 -0
- package/dist/slices/estimate/index.js +32 -1
- package/dist/slices/estimate/requests.js +8 -0
- package/dist/slices/library/model.js +2 -3
- package/dist/slices/library/slots.js +3 -1
- package/dist/slices/narration/preparation.js +75 -0
- package/dist/slices/narration/steering.js +89 -0
- package/dist/slices/play-drafts/convert.js +3 -0
- package/dist/slices/play-drafts/readiness.js +3 -1
- package/dist/slices/play-drafts/schema.js +1 -0
- package/dist/slices/project-templates/from-project.js +2 -0
- package/dist/slices/project-templates/setup.js +1 -0
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/preview-details.js +12 -9
- package/dist/slices/rebuild/preview-plan.js +7 -1
- package/dist/slices/rebuild/recipe-audio-parts.js +89 -0
- package/dist/slices/rebuild/recipe-audio.js +52 -66
- package/dist/slices/rebuild/recipe-input-schema.js +11 -0
- package/dist/slices/rebuild/recipe-legacy.js +5 -0
- package/dist/slices/rebuild/recipe-model.js +3 -0
- package/dist/slices/rebuild/recipe-narration-text.js +33 -0
- package/dist/slices/rebuild/recipe-preparation.js +66 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +7 -5
- package/dist/slices/rebuild/recipe-validation.js +9 -1
- package/dist/slices/rebuild/recipe-work.js +13 -1
- package/dist/slices/rebuild/runtime-admission.js +11 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +4 -0
- package/dist/slices/rebuild/runtime-local.js +5 -0
- package/dist/slices/rebuild/runtime-materialize.js +2 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +1 -0
- package/dist/slices/rebuild/runtime-narration-text.js +75 -0
- package/dist/slices/rebuild/runtime-provider.js +30 -1
- package/dist/slices/rebuild/runtime-publication.js +13 -1
- package/dist/slices/rebuild/runtime-store.js +13 -2
- package/dist/slices/revisions/rules.js +5 -3
- package/dist/slices/settings/cli-paths.js +10 -6
- package/dist/slices/settings/cli-status.js +1 -1
- package/dist/slices/settings/model.js +9 -0
- package/dist/slices/settings/readiness.js +9 -2
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/layout.js +4 -0
- package/dist/slices/storage/model.js +2 -0
- package/dist/slices/storage/schema.js +1 -0
- package/dist/web/assets/index-DsumGtv6.js +137 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-Cq2O1JRf.js +0 -137
|
@@ -6,6 +6,7 @@ import { splitEndMatter } from "../article/split.js";
|
|
|
6
6
|
import { joinNarration } from "../narration/concat.js";
|
|
7
7
|
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
8
8
|
import { outputPath } from "../storage/layout.js";
|
|
9
|
+
import { publishNarrationText } from "./runtime-narration-text.js";
|
|
9
10
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
10
11
|
import { preparedResult, preparedText, preparedTexts, publishResult, } from "./runtime-publication.js";
|
|
11
12
|
export async function executeLocalRecipe(deps, context, piece) {
|
|
@@ -43,6 +44,10 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
43
44
|
}
|
|
44
45
|
if (input.kind !== "local")
|
|
45
46
|
throw new Error("Expected an exact local recipe.");
|
|
47
|
+
if (input.operation === "narration-files-v1") {
|
|
48
|
+
await publishNarrationText(deps, context, piece);
|
|
49
|
+
return "done";
|
|
50
|
+
}
|
|
46
51
|
if (input.operation === "concat-narration") {
|
|
47
52
|
await concatenate(deps, context, piece);
|
|
48
53
|
return "done";
|
|
@@ -95,6 +95,8 @@ function desiredAnchor(deps, projectId, head, row) {
|
|
|
95
95
|
(row.desired_fingerprint ?? row.fingerprint));
|
|
96
96
|
}
|
|
97
97
|
function futureKey(recipe) {
|
|
98
|
+
if (recipe.input.kind === "llm" && recipe.input.preparation !== undefined)
|
|
99
|
+
return `narration:prepare:${recipe.input.preparation.segment}:future`;
|
|
98
100
|
if (recipe.input.kind === "tts")
|
|
99
101
|
return `audio:${recipe.input.segment}:future`;
|
|
100
102
|
if (recipe.key.startsWith("research:chapter:"))
|
|
@@ -112,6 +112,7 @@ export function bindNarrationReuse(deps, view, recipe, ordinal) {
|
|
|
112
112
|
payload: JSON.stringify({
|
|
113
113
|
...(typeof payload === "object" && payload !== null ? payload : {}),
|
|
114
114
|
text: input.text,
|
|
115
|
+
spokenText: input.spokenText,
|
|
115
116
|
logicalKey: input.logicalKey,
|
|
116
117
|
logicalText: input.logicalText,
|
|
117
118
|
segment: input.segment,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
3
|
+
import { preparedTexts, publishResult } from "./runtime-publication.js";
|
|
4
|
+
export function joinedNarration(parts) {
|
|
5
|
+
return parts
|
|
6
|
+
.map((part, index) => `${index > 0 && parts[index - 1]?.logicalKey !== part.logicalKey ? "\n" : ""}${part.spokenText}`)
|
|
7
|
+
.join("");
|
|
8
|
+
}
|
|
9
|
+
export function narrationTextParts(view, plan, segment) {
|
|
10
|
+
const concat = plan.recipes.find((row) => row.key === (segment === "body" ? "audio:body:concat" : `audio:${segment}`));
|
|
11
|
+
if (concat === undefined)
|
|
12
|
+
throw new Error(`The saved ${segment} narration is missing.`);
|
|
13
|
+
return concat.dependsOn.map((key) => {
|
|
14
|
+
const recipe = plan.recipes.find((row) => row.key === key);
|
|
15
|
+
const selected = view.pieces.find((row) => row.key === key &&
|
|
16
|
+
row.selected &&
|
|
17
|
+
row.available &&
|
|
18
|
+
row.piece.state === "done" &&
|
|
19
|
+
row.fingerprint === recipe?.fingerprint);
|
|
20
|
+
if (selected === undefined || recipe === undefined)
|
|
21
|
+
throw new Error("The saved narration transcript is incomplete.");
|
|
22
|
+
const input = recipe.input;
|
|
23
|
+
if (input.kind === "provided" &&
|
|
24
|
+
Array.isArray(input.semantic) &&
|
|
25
|
+
typeof input.semantic[0] === "string")
|
|
26
|
+
return {
|
|
27
|
+
logicalKey: key.slice(0, key.lastIndexOf(":")),
|
|
28
|
+
spokenText: input.semantic[0],
|
|
29
|
+
requestText: null,
|
|
30
|
+
};
|
|
31
|
+
if (input.kind !== "tts")
|
|
32
|
+
throw new Error("The narration source has not resolved.");
|
|
33
|
+
const payload = z
|
|
34
|
+
.object({ text: z.string().optional(), spokenText: z.string().optional() })
|
|
35
|
+
.parse(JSON.parse(selected.piece.payload ?? "{}"));
|
|
36
|
+
if (input.spokenText !== undefined &&
|
|
37
|
+
(payload.spokenText !== input.spokenText || payload.text !== input.text))
|
|
38
|
+
throw new Error("Prepared narration is missing its exact clean transcript.");
|
|
39
|
+
return {
|
|
40
|
+
logicalKey: input.logicalKey,
|
|
41
|
+
spokenText: input.spokenText === undefined ? (payload.text ?? input.text) : (payload.spokenText ?? ""),
|
|
42
|
+
requestText: input.text,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function publishNarrationText(deps, context, piece) {
|
|
47
|
+
if (piece.input.kind !== "local")
|
|
48
|
+
throw new Error("Expected a narration-file recipe.");
|
|
49
|
+
const { segment } = z
|
|
50
|
+
.object({ segment: z.enum(["body", "intro", "outro"]) })
|
|
51
|
+
.parse(piece.input.values);
|
|
52
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
53
|
+
const row = deps.db
|
|
54
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
55
|
+
.get(context.work.workId);
|
|
56
|
+
if (view === undefined)
|
|
57
|
+
throw new Error("The pinned narration revision is missing.");
|
|
58
|
+
const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
|
|
59
|
+
if (!plan.recipes.some((recipe) => recipe.key === piece.key && recipe.fingerprint === piece.fingerprint))
|
|
60
|
+
throw new Error("Narration text no longer matches the admitted inputs.");
|
|
61
|
+
const parts = narrationTextParts(view, plan, segment);
|
|
62
|
+
const outputs = preparedTexts(deps, context, piece, [
|
|
63
|
+
["narration_txt", `${segment}-narration.txt`, joinedNarration(parts)],
|
|
64
|
+
[
|
|
65
|
+
"tts_script",
|
|
66
|
+
`${segment}-tts-script.txt`,
|
|
67
|
+
parts.flatMap((part) => (part.requestText === null ? [] : [part.requestText])).join("\n\n"),
|
|
68
|
+
],
|
|
69
|
+
]).map((output) => ({
|
|
70
|
+
...output,
|
|
71
|
+
slot: `${piece.key}:${output.output.role}`,
|
|
72
|
+
output: { ...output.output, meta: { ...output.output.meta, segment } },
|
|
73
|
+
}));
|
|
74
|
+
await publishResult(deps, context, piece, outputs, { segment });
|
|
75
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { plainText } from "../article/plain.js";
|
|
2
2
|
import { splitEndMatter } from "../article/split.js";
|
|
3
3
|
import { observeNarration } from "../narration/live.js";
|
|
4
|
+
import { validatePreparation } from "../narration/preparation.js";
|
|
5
|
+
import { prepareRequests } from "../narration/steering.js";
|
|
4
6
|
import { chaptersFrom } from "../research/planner.js";
|
|
5
7
|
import { sourcedAnswer } from "../research/synthesis.js";
|
|
6
8
|
import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
@@ -28,7 +30,7 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
|
|
|
28
30
|
messages: input.messages,
|
|
29
31
|
webSearch: input.webSearch,
|
|
30
32
|
previewLabel: piece.key,
|
|
31
|
-
check: (value) => checkAnswer(piece, value),
|
|
33
|
+
check: (value) => checkAnswer(piece, value) ?? checkPreparation(deps, context, piece, value),
|
|
32
34
|
});
|
|
33
35
|
if (!answer.ok)
|
|
34
36
|
return "held";
|
|
@@ -63,6 +65,7 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
|
|
|
63
65
|
: await probeDurationMs(deps.ffmpeg, outputPath(deps.paths, context.work.projectId, asset.path), context.signal, deps.log);
|
|
64
66
|
await publishResult(deps, context, piece, [], {
|
|
65
67
|
text: input.text,
|
|
68
|
+
...(input.spokenText === undefined ? {} : { spokenText: input.spokenText }),
|
|
66
69
|
logicalKey: input.logicalKey,
|
|
67
70
|
logicalText: input.logicalText,
|
|
68
71
|
segment: input.segment,
|
|
@@ -125,6 +128,18 @@ function checkAnswer(piece, answer) {
|
|
|
125
128
|
return undefined;
|
|
126
129
|
}
|
|
127
130
|
async function publishText(deps, context, piece, answer) {
|
|
131
|
+
if (piece.input.kind === "llm" && piece.input.preparation !== undefined) {
|
|
132
|
+
const checked = validatePreparation(answer.text, piece.input.preparation.source);
|
|
133
|
+
if (!checked.ok)
|
|
134
|
+
throw new Error(checked.reason);
|
|
135
|
+
await publishResult(deps, context, piece, [], {
|
|
136
|
+
text: answer.text,
|
|
137
|
+
preparation: piece.input.preparation,
|
|
138
|
+
cues: checked.cues,
|
|
139
|
+
logicalFingerprint: piece.logicalFingerprint ?? piece.fingerprint,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
128
143
|
if (piece.key === "article:body") {
|
|
129
144
|
const parts = splitEndMatter(answer.text);
|
|
130
145
|
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
@@ -182,3 +197,17 @@ function narrationLabel(deps, context, piece) {
|
|
|
182
197
|
const index = parts.findIndex((recipe) => recipe.key === piece.key);
|
|
183
198
|
return parts.length > 1 && index >= 0 ? `${name} part ${index + 1} of ${parts.length}` : name;
|
|
184
199
|
}
|
|
200
|
+
function checkPreparation(deps, context, piece, answer) {
|
|
201
|
+
if (piece.input.kind !== "llm" || piece.input.preparation === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
const source = piece.input.preparation.source;
|
|
204
|
+
const checked = validatePreparation(answer.text, source);
|
|
205
|
+
if (!checked.ok)
|
|
206
|
+
return checked.reason;
|
|
207
|
+
const row = deps.db
|
|
208
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
209
|
+
.get(context.work.workId);
|
|
210
|
+
const model = savedCatalogue(row?.recipe_context).tts.find((model) => model.provider === "inworld" && model.id === "inworld-tts-2");
|
|
211
|
+
const prepared = prepareRequests(source, checked.cues, model?.tts.maxCharacters ?? 4000);
|
|
212
|
+
return prepared.ok ? undefined : prepared.reason;
|
|
213
|
+
}
|
|
@@ -80,6 +80,8 @@ export async function publishResult(deps, context, piece, outputs, payload, asse
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
function pieceKind(piece) {
|
|
83
|
+
if (piece.input.kind === "llm" && piece.input.preparation !== undefined)
|
|
84
|
+
return "prompt_written";
|
|
83
85
|
if (piece.input.kind === "tts")
|
|
84
86
|
return piece.input.segment === "body" ? "chunk" : "segment";
|
|
85
87
|
if (piece.input.kind === "provided" && /^audio:body:.+:\d+$/.test(piece.key))
|
|
@@ -97,6 +99,10 @@ function pieceKind(piece) {
|
|
|
97
99
|
return "article_written";
|
|
98
100
|
}
|
|
99
101
|
function pieceIndex(deps, context, piece) {
|
|
102
|
+
if (piece.input.kind === "llm" && piece.input.preparation?.segment === "intro")
|
|
103
|
+
return 1;
|
|
104
|
+
if (piece.input.kind === "llm" && piece.input.preparation?.segment === "outro")
|
|
105
|
+
return 2;
|
|
100
106
|
// Intro and outro can resolve independently. Reserve alternating ordinals so a
|
|
101
107
|
// late intro part cannot collide with an outro that was already published.
|
|
102
108
|
const segment = /^audio:(intro|outro):(\d+)$/.exec(piece.key);
|
|
@@ -116,6 +122,9 @@ function pieceIndex(deps, context, piece) {
|
|
|
116
122
|
"subtitles:files",
|
|
117
123
|
"export:wav",
|
|
118
124
|
"export:video",
|
|
125
|
+
"narration:files:body",
|
|
126
|
+
"narration:files:intro",
|
|
127
|
+
"narration:files:outro",
|
|
119
128
|
];
|
|
120
129
|
const localIndex = localOrder.indexOf(piece.key);
|
|
121
130
|
if (localIndex >= 0)
|
|
@@ -126,10 +135,13 @@ function pieceIndex(deps, context, piece) {
|
|
|
126
135
|
.get(context.work.workId);
|
|
127
136
|
if (view !== undefined && row?.recipe_context !== null && row?.recipe_context !== undefined) {
|
|
128
137
|
const keys = executionPlan(deps, view, savedCatalogue(row.recipe_context)).recipes.filter((recipe) => recipe.stage === context.work.kind &&
|
|
138
|
+
(piece.input.kind !== "llm" ||
|
|
139
|
+
piece.input.preparation?.segment !== "body" ||
|
|
140
|
+
(recipe.input.kind === "llm" && recipe.input.preparation?.segment === "body")) &&
|
|
129
141
|
pieceKind({ ...piece, key: recipe.key, input: recipe.input }) === pieceKind(piece));
|
|
130
142
|
const index = keys.findIndex((recipe) => recipe.key === piece.key);
|
|
131
143
|
if (index >= 0)
|
|
132
|
-
return index + 1;
|
|
144
|
+
return (index + (piece.input.kind === "llm" && piece.input.preparation?.segment === "body" ? 3 : 1));
|
|
133
145
|
}
|
|
134
146
|
const last = piece.key.split(":").at(-1);
|
|
135
147
|
return last !== undefined && /^\d+$/.test(last) ? Number(last) : 1;
|
|
@@ -26,6 +26,7 @@ export function executionStages(deps, projectId) {
|
|
|
26
26
|
.prepare("SELECT work_key FROM revision_work_reservations WHERE revision_id=?")
|
|
27
27
|
.all(head)
|
|
28
28
|
.map((row) => String(row.work_key)));
|
|
29
|
+
const futureKeys = new Map();
|
|
29
30
|
return rows
|
|
30
31
|
.filter((row) => workPieces(deps.db, String(row.id)).some((piece) => {
|
|
31
32
|
if (!keys.has(piece.key))
|
|
@@ -33,8 +34,18 @@ export function executionStages(deps, projectId) {
|
|
|
33
34
|
if (piece.key === "subtitles:timing" && view?.revision.content.subtitleCues !== undefined)
|
|
34
35
|
return false;
|
|
35
36
|
if (piece.input.kind === "deferred" && piece.key.endsWith(":future")) {
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const cacheKey = String(row.id);
|
|
38
|
+
let deferred = futureKeys.get(cacheKey);
|
|
39
|
+
if (deferred === undefined) {
|
|
40
|
+
const origin = executionView(deps, projectId, String(row.revision_id));
|
|
41
|
+
if (origin === undefined || row.recipe_context === null)
|
|
42
|
+
return true;
|
|
43
|
+
deferred = new Set(executionPlan(deps, origin, savedCatalogue(row.recipe_context))
|
|
44
|
+
.recipes.filter((recipe) => recipe.deferred)
|
|
45
|
+
.map((recipe) => recipe.key));
|
|
46
|
+
futureKeys.set(cacheKey, deferred);
|
|
47
|
+
}
|
|
48
|
+
return deferred.has(piece.key);
|
|
38
49
|
}
|
|
39
50
|
return true;
|
|
40
51
|
}))
|
|
@@ -79,9 +79,11 @@ export function validateNarrationIntent(base, edit) {
|
|
|
79
79
|
});
|
|
80
80
|
const keys = new Set(recipes.flatMap((row) => row.input.kind === "tts"
|
|
81
81
|
? [row.input.logicalKey]
|
|
82
|
-
: row.
|
|
83
|
-
? [row.
|
|
84
|
-
:
|
|
82
|
+
: row.input.kind === "llm" && row.input.preparation !== undefined
|
|
83
|
+
? [row.input.preparation.logicalKey]
|
|
84
|
+
: row.stage === "audio" && row.key.endsWith(":1")
|
|
85
|
+
? [row.key.slice(0, -2)]
|
|
86
|
+
: []));
|
|
85
87
|
const fields = [];
|
|
86
88
|
for (const [key, value] of Object.entries(edit.content.narrationOverrides))
|
|
87
89
|
if (!isDeepStrictEqual(value, base.revision.content.narrationOverrides[key]) && !keys.has(key))
|
|
@@ -13,11 +13,14 @@ const pathSchema = z
|
|
|
13
13
|
.refine((value) => value === "" || (isAbsolute(value) && !/[\p{Cc}]/u.test(value)));
|
|
14
14
|
const storedPathSchema = z.string().max(cliPathMaxLength).nullable();
|
|
15
15
|
const saves = new WeakMap();
|
|
16
|
+
function installationId(id) {
|
|
17
|
+
return id === "codex-image" ? "codex" : id;
|
|
18
|
+
}
|
|
16
19
|
export function cliPathStatus(db, id) {
|
|
17
20
|
const provider = providerById(id);
|
|
18
21
|
if (provider.auth !== "cli")
|
|
19
22
|
throw new Error(`${id} is not a CLI provider`);
|
|
20
|
-
const stored = readSetting(db, `cli.path.${id}`);
|
|
23
|
+
const stored = readSetting(db, `cli.path.${installationId(id)}`);
|
|
21
24
|
const configured = stored === undefined ? null : storedPathSchema.parse(JSON.parse(stored));
|
|
22
25
|
return { configured, command: configured ?? provider.binary };
|
|
23
26
|
}
|
|
@@ -32,16 +35,17 @@ export async function saveCliPath(deps, id, raw) {
|
|
|
32
35
|
queue = new Map();
|
|
33
36
|
saves.set(deps.db, queue);
|
|
34
37
|
}
|
|
35
|
-
const
|
|
38
|
+
const sharedId = installationId(id);
|
|
39
|
+
const previous = queue.get(sharedId) ?? Promise.resolve();
|
|
36
40
|
const next = previous.then(() => save(deps, id, raw));
|
|
37
41
|
const settled = next.then(() => { }, () => { });
|
|
38
|
-
queue.set(
|
|
42
|
+
queue.set(sharedId, settled);
|
|
39
43
|
try {
|
|
40
44
|
return await next;
|
|
41
45
|
}
|
|
42
46
|
finally {
|
|
43
|
-
if (queue.get(
|
|
44
|
-
queue.delete(
|
|
47
|
+
if (queue.get(sharedId) === settled)
|
|
48
|
+
queue.delete(sharedId);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
async function save(deps, id, raw) {
|
|
@@ -84,7 +88,7 @@ async function save(deps, id, raw) {
|
|
|
84
88
|
message: probed.error ??
|
|
85
89
|
`${provider.displayName} did not answer --version successfully within ${cliProbeTimeoutMs / 1000} seconds. Choose its executable file without arguments.`,
|
|
86
90
|
};
|
|
87
|
-
writeSetting(deps.db, `cli.path.${id}`, JSON.stringify(configured));
|
|
91
|
+
writeSetting(deps.db, `cli.path.${installationId(id)}`, JSON.stringify(configured));
|
|
88
92
|
return {
|
|
89
93
|
ok: true,
|
|
90
94
|
status: {
|
|
@@ -35,7 +35,7 @@ export function readinessFromProbe(result, provider) {
|
|
|
35
35
|
return { kind: "cli", installed: false };
|
|
36
36
|
}
|
|
37
37
|
const version = versionFrom(result.stdout);
|
|
38
|
-
if (provider.id !== "codex")
|
|
38
|
+
if (provider.id !== "codex" && provider.id !== "codex-image")
|
|
39
39
|
return version === undefined
|
|
40
40
|
? { kind: "cli", installed: true }
|
|
41
41
|
: { kind: "cli", installed: true, version };
|
|
@@ -17,6 +17,7 @@ export const providerIds = [
|
|
|
17
17
|
"replicate",
|
|
18
18
|
"openai-image",
|
|
19
19
|
"google-image",
|
|
20
|
+
"codex-image",
|
|
20
21
|
];
|
|
21
22
|
export function isLocalCliProvider(id) {
|
|
22
23
|
return id === "claude-code" || id === "codex" || id === "gemini" || id === "codex-image";
|
|
@@ -55,6 +56,14 @@ export const providers = [
|
|
|
55
56
|
{ id: "replicate", family: "image", displayName: "Replicate", auth: "key" },
|
|
56
57
|
{ id: "openai-image", family: "image", displayName: "OpenAI", auth: "key" },
|
|
57
58
|
{ id: "google-image", family: "image", displayName: "Google", auth: "key" },
|
|
59
|
+
{
|
|
60
|
+
id: "codex-image",
|
|
61
|
+
family: "image",
|
|
62
|
+
displayName: "Codex CLI",
|
|
63
|
+
auth: "cli",
|
|
64
|
+
binary: "codex",
|
|
65
|
+
versionArgs: ["--version"],
|
|
66
|
+
},
|
|
58
67
|
];
|
|
59
68
|
export function providerById(id) {
|
|
60
69
|
const found = providers.find((provider) => provider.id === id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cliPathStatus } from "./cli-paths.js";
|
|
2
|
-
import {
|
|
2
|
+
import { cliProbeTimeoutMs, readinessFromProbe } from "./cli-status.js";
|
|
3
3
|
import { providers } from "./model.js";
|
|
4
4
|
import { keyedProviders } from "./repo.js";
|
|
5
5
|
// Every supported provider is listed, keyed or not, found or not, so Play can grey one out with
|
|
@@ -7,15 +7,22 @@ import { keyedProviders } from "./repo.js";
|
|
|
7
7
|
// when its binary answers. Readiness is computed per request using the latest saved executable override.
|
|
8
8
|
export async function providerStatuses(deps) {
|
|
9
9
|
const keyed = keyedProviders(deps.db);
|
|
10
|
+
const probes = new Map();
|
|
10
11
|
return await Promise.all(providers.map(async (provider) => {
|
|
11
12
|
const base = { id: provider.id, family: provider.family, displayName: provider.displayName };
|
|
12
13
|
if (provider.auth !== "cli")
|
|
13
14
|
return { ...base, readiness: keyedReadiness(keyed, provider.id) };
|
|
14
15
|
const cliPath = cliPathStatus(deps.db, provider.id);
|
|
16
|
+
const probeKey = JSON.stringify([cliPath.command, provider.versionArgs]);
|
|
17
|
+
let result = probes.get(probeKey);
|
|
18
|
+
if (result === undefined) {
|
|
19
|
+
result = deps.probe(cliPath.command, provider.versionArgs, cliProbeTimeoutMs);
|
|
20
|
+
probes.set(probeKey, result);
|
|
21
|
+
}
|
|
15
22
|
return {
|
|
16
23
|
...base,
|
|
17
24
|
cliPath,
|
|
18
|
-
readiness: await
|
|
25
|
+
readiness: readinessFromProbe(await result, provider),
|
|
19
26
|
};
|
|
20
27
|
}));
|
|
21
28
|
}
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// research and the article each store what they sent.
|
|
8
8
|
export function assetOf(output) {
|
|
9
9
|
const asset = output.role.replaceAll("_", "-");
|
|
10
|
+
if ((output.role === "narration_txt" || output.role === "tts_script") &&
|
|
11
|
+
output.meta.segment !== undefined)
|
|
12
|
+
return `${asset}-${output.meta.segment}`;
|
|
10
13
|
if (output.role === "instructions") {
|
|
11
14
|
return `${output.stageKind}-${asset}`;
|
|
12
15
|
}
|
|
@@ -30,6 +30,10 @@ export function outputFileName(role, index, extension, stageKind) {
|
|
|
30
30
|
return "article.md";
|
|
31
31
|
case "article_txt":
|
|
32
32
|
return "article.txt";
|
|
33
|
+
case "narration_txt":
|
|
34
|
+
return "narration.txt";
|
|
35
|
+
case "tts_script":
|
|
36
|
+
return "tts-script.txt";
|
|
33
37
|
case "sources":
|
|
34
38
|
return "sources.txt";
|
|
35
39
|
case "glossary":
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { outputRoles, stagedFileStates, stageKinds } from "./model.js";
|
|
3
3
|
export const metaSchema = z.object({
|
|
4
|
+
segment: z.enum(["body", "intro", "outro"]).optional(),
|
|
4
5
|
subtitleOmissions: z
|
|
5
6
|
.array(z.object({ start: z.number().finite().nonnegative(), text: z.string() }))
|
|
6
7
|
.optional(),
|