@gentbajko/slopify 1.2.0 → 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.
Files changed (61) hide show
  1. package/README.md +12 -2
  2. package/dist/adapter-registry.js +10 -2
  3. package/dist/adapters/image/codex.js +230 -0
  4. package/dist/adapters/llm/claude-code-models.js +24 -13
  5. package/dist/adapters/llm/codex-models.js +12 -5
  6. package/dist/adapters/llm/codex-server-models.js +150 -0
  7. package/dist/adapters/llm/codex.js +1 -1
  8. package/dist/adapters/llm/gemini-models.js +23 -19
  9. package/dist/catalog/validate.js +3 -1
  10. package/dist/edge/cli.js +17 -0
  11. package/dist/kernel/db/migrations/0011-narration-prompts.sql +13 -0
  12. package/dist/slices/admission/rules.js +17 -1
  13. package/dist/slices/admission/schema.js +1 -0
  14. package/dist/slices/estimate/index.js +32 -1
  15. package/dist/slices/estimate/requests.js +8 -0
  16. package/dist/slices/library/model.js +2 -3
  17. package/dist/slices/library/slots.js +3 -1
  18. package/dist/slices/narration/preparation.js +75 -0
  19. package/dist/slices/narration/steering.js +89 -0
  20. package/dist/slices/play-drafts/convert.js +3 -0
  21. package/dist/slices/play-drafts/readiness.js +3 -1
  22. package/dist/slices/play-drafts/schema.js +1 -0
  23. package/dist/slices/project-templates/from-project.js +2 -0
  24. package/dist/slices/project-templates/setup.js +1 -0
  25. package/dist/slices/rebuild/admission-repo.js +2 -2
  26. package/dist/slices/rebuild/preview-details.js +12 -9
  27. package/dist/slices/rebuild/preview-plan.js +7 -1
  28. package/dist/slices/rebuild/recipe-audio-parts.js +89 -0
  29. package/dist/slices/rebuild/recipe-audio.js +52 -66
  30. package/dist/slices/rebuild/recipe-input-schema.js +11 -0
  31. package/dist/slices/rebuild/recipe-legacy.js +5 -0
  32. package/dist/slices/rebuild/recipe-model.js +3 -0
  33. package/dist/slices/rebuild/recipe-narration-text.js +33 -0
  34. package/dist/slices/rebuild/recipe-preparation.js +66 -0
  35. package/dist/slices/rebuild/recipe-provider-choice.js +7 -5
  36. package/dist/slices/rebuild/recipe-validation.js +9 -1
  37. package/dist/slices/rebuild/recipe-work.js +13 -1
  38. package/dist/slices/rebuild/runtime-admission.js +11 -0
  39. package/dist/slices/rebuild/runtime-export-inputs.js +4 -0
  40. package/dist/slices/rebuild/runtime-local.js +5 -0
  41. package/dist/slices/rebuild/runtime-materialize.js +2 -0
  42. package/dist/slices/rebuild/runtime-narration-reuse.js +1 -0
  43. package/dist/slices/rebuild/runtime-narration-text.js +75 -0
  44. package/dist/slices/rebuild/runtime-provider.js +30 -1
  45. package/dist/slices/rebuild/runtime-publication.js +13 -1
  46. package/dist/slices/rebuild/runtime-store.js +13 -2
  47. package/dist/slices/revisions/rules.js +5 -3
  48. package/dist/slices/settings/cli-paths.js +10 -6
  49. package/dist/slices/settings/cli-status.js +1 -1
  50. package/dist/slices/settings/model.js +9 -0
  51. package/dist/slices/settings/models.js +3 -3
  52. package/dist/slices/settings/readiness.js +9 -2
  53. package/dist/slices/storage/asset-name.js +3 -0
  54. package/dist/slices/storage/layout.js +4 -0
  55. package/dist/slices/storage/model.js +2 -0
  56. package/dist/slices/storage/schema.js +1 -0
  57. package/dist/web/assets/index-DsumGtv6.js +137 -0
  58. package/dist/web/index.html +1 -1
  59. package/package.json +2 -1
  60. package/scripts/docker-run.sh +113 -0
  61. package/dist/web/assets/index-DwM-Y6nw.js +0 -137
@@ -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 prefix = piece.key.slice(0, -"future".length);
37
- return !rows.some((candidate) => workPieces(deps.db, String(candidate.id)).some((part) => part.input.kind === "tts" && part.key.startsWith(prefix)));
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.stage === "audio" && row.key.endsWith(":1")
83
- ? [row.key.slice(0, -2)]
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 previous = queue.get(id) ?? Promise.resolve();
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(id, settled);
42
+ queue.set(sharedId, settled);
39
43
  try {
40
44
  return await next;
41
45
  }
42
46
  finally {
43
- if (queue.get(id) === settled)
44
- queue.delete(id);
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);
@@ -82,9 +82,9 @@ function uniqueModels(models) {
82
82
  }
83
83
  function catalogNotice(provider) {
84
84
  const notices = {
85
- codex: "Models come from Codex's local catalogue. Open Codex to refresh that catalogue, then refresh this list.",
86
- gemini: "Models and aliases come from the installed Gemini CLI. Update that CLI to discover newer models.",
87
- "claude-code": "These aliases follow Claude Code's model families. You can also enter an exact model ID.",
85
+ codex: "Models come from your Codex installation. Refresh to check again.",
86
+ gemini: "Models come from your installed Gemini CLI, not an account access check. Missing a new model? Update Gemini CLI, rerun the Docker launcher if applicable, then refresh this list.",
87
+ "claude-code": "Models come from Claude Code. Missing a new model? Update Claude Code, rerun the Docker launcher if applicable, then refresh this list. You can also enter an exact model ID.",
88
88
  inworld: "These are Inworld’s documented TTS models. You can also enter a compatible model ID.",
89
89
  cartesia: "Cartesia has no model-list API. These are bundled compatible choices; you can enter a newer model ID.",
90
90
  fal: "These models have input formats supported by Slopify's fal adapter.",
@@ -1,5 +1,5 @@
1
1
  import { cliPathStatus } from "./cli-paths.js";
2
- import { cliReadiness } from "./cli-status.js";
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 cliReadiness(deps.probe, { ...provider, binary: cliPath.command }),
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":
@@ -6,6 +6,8 @@ export const outputRoles = [
6
6
  "notes",
7
7
  "article_md",
8
8
  "article_txt",
9
+ "narration_txt",
10
+ "tts_script",
9
11
  "sources",
10
12
  "glossary",
11
13
  "audio_body",
@@ -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(),