@gentbajko/slopify 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -149,6 +149,8 @@ export function codexImageArgs(req, directory) {
149
149
  codexImageInstructions(req, reference, directory),
150
150
  ];
151
151
  }
152
+ // How a blocked image shows: the tool's error on stderr, or the agent's reply.
153
+ const blocked = /moderation_blocked|rejected by the safety system|safety (?:filter|system)|content polic|refus/i;
152
154
  const failure = z.object({ error: z.object({ message: z.string() }) });
153
155
  const turnUsage = z.object({
154
156
  usage: z
@@ -226,6 +228,8 @@ export function codexImage(deps) {
226
228
  }
227
229
  let completed = false;
228
230
  let unavailable = false;
231
+ // What the agent said last: when its image tool is blocked it draws nothing and says why.
232
+ let said = "";
229
233
  for await (const line of lines(bounded(run.stdout, maxEventBytes), req.signal)) {
230
234
  if (line.trim() === "")
231
235
  continue;
@@ -288,6 +292,8 @@ export function codexImage(deps) {
288
292
  }
289
293
  else if (event.type === "item.completed") {
290
294
  const { item: value } = cliShaped(binary, item, event.value);
295
+ if (value.type === "agent_message" && value.text)
296
+ said = value.text;
291
297
  if (value.type === "agent_message" &&
292
298
  /image.generation.*unavailable|cannot generate images|no image tool/i.test(value.text ?? ""))
293
299
  unavailable = true;
@@ -315,6 +321,14 @@ export function codexImage(deps) {
315
321
  kind: "unsupported",
316
322
  message: `The Codex CLI cannot make images. ${cannotDraw}`,
317
323
  });
324
+ // The image tool's safety system refused every picture, so the run ended without one.
325
+ if (threadId !== undefined &&
326
+ codexImageCount(env, threadId) === 0 &&
327
+ (blocked.test(run.stderr()) || blocked.test(said)))
328
+ throw providerError({
329
+ kind: "refusal",
330
+ message: refusedImage("The Codex CLI", redact(said.trim())),
331
+ });
318
332
  const image = codexGeneratedImage(env, threadId, startedAt);
319
333
  const limits = await readingOf(before, deps.readLimits);
320
334
  return {
@@ -11,7 +11,7 @@ import { revisionControlSchema, } from "../../slices/control/revision-control-sc
11
11
  import { recoverProject } from "../../slices/rebuild/recovery.js";
12
12
  import { recoveryResultSchema } from "../../slices/rebuild/recovery-model.js";
13
13
  import { resumable } from "../../slices/rebuild/recovery-repo.js";
14
- import { clearSoftening, refusedKeys, requestSoftening } from "../../slices/rebuild/soften.js";
14
+ import { clearSoftening, requestSoftening, softenableKeys } from "../../slices/rebuild/soften.js";
15
15
  import { adoptBaseline } from "../../slices/revisions/adopt.js";
16
16
  import { currentRevisionId } from "../../slices/revisions/repo.js";
17
17
  import { allowsCustomModel } from "../../slices/settings/models.js";
@@ -218,7 +218,7 @@ export function actionRoutes(deps) {
218
218
  // AI model when the retry draws them again (`slices/rebuild/soften.ts`).
219
219
  .post("/:id/stages/:kind/soften", zValidator("param", stageParam, onInvalid), zValidator("json", revisionControlSchema, onInvalid), async (c) => {
220
220
  const { id, kind } = c.req.valid("param");
221
- const keys = kind === "images" || kind === "thumbnail" ? refusedKeys(deps.db, id, kind) : [];
221
+ const keys = softenableKeys(deps.db, id, kind);
222
222
  if (keys.length === 0)
223
223
  return problem(c, {
224
224
  status: 409,
@@ -0,0 +1,13 @@
1
+ # Slopify 3.2.1
2
+
3
+ Released 30 September 2026.
4
+
5
+ 3.2.1 lets one narration chunk be re-tagged without re-recording the whole narration.
6
+
7
+ ## Narration
8
+
9
+ - A chunk whose text is edited in Edit project → Narration can carry its own delivery note. The note is added to the narration prep prompt for that chunk only, so its delivery cues can change (a calmer opening, say) while every other chunk keeps its recording.
10
+
11
+ ## Upgrading from 3.2.0
12
+
13
+ - **Nothing becomes outdated.**
@@ -0,0 +1,15 @@
1
+ # Slopify 3.2.2
2
+
3
+ Released 1 October 2026.
4
+
5
+ 3.2.2 names a refused short's picture for what it is and lets Soften and retry fix it.
6
+
7
+ ## Shorts
8
+
9
+ - When the Codex CLI's image tool refuses a picture under its safety rules, the step now says so ("refused to make this image under its content rules", with Codex's own reason) instead of "the saved file could not be read".
10
+ - A short's refused picture can be fixed with **Soften and retry** in the Video section, as Images and Thumbnail pictures already could: the project's AI model rewords the prompt without what the filter flagged, then the picture is drawn again.
11
+
12
+ ## Upgrading from 3.2.1
13
+
14
+ - **Nothing becomes outdated.**
15
+ - A short that already failed with "the saved file could not be read" needs one more **Try again**; if the picture is refused again, the Soften and retry button appears.
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "id": "3.2.2",
4
+ "title": "Slopify 3.2.2",
5
+ "version": "3.2.2",
6
+ "date": "2026-10-01"
7
+ },
8
+ {
9
+ "id": "3.2.1",
10
+ "title": "Slopify 3.2.1",
11
+ "version": "3.2.1",
12
+ "date": "2026-09-30"
13
+ },
2
14
  {
3
15
  "id": "3.2.0",
4
16
  "title": "Slopify 3.2.0",
@@ -11,6 +11,8 @@ const retiredModel = /\bunknown model\b|\bmodel\b[^\n]{0,80}?\b(?:not found|does
11
11
  const keyTrouble = /did not accept the API key|API key is saved|invalid api key|key may be wrong/i;
12
12
  // Stages whose failed call draws an image from a prompt, which Soften and retry can reword.
13
13
  const drawn = ["images", "thumbnail"];
14
+ // A short's still is drawn in the Video stage; its failure names it ("Short 4 image 6: …").
15
+ const shortStill = /^Short \d+ image \d+:/;
14
16
  export function fixFor(step) {
15
17
  const reason = step.reason ?? "";
16
18
  const cli = cliOf(step.provider, reason);
@@ -24,7 +26,7 @@ export function fixFor(step) {
24
26
  if (diskFull.test(reason))
25
27
  return { kind: "free-space", label: "Free space" };
26
28
  if (step.kind === "refusal" || refusedWords.test(reason))
27
- return drawn.includes(step.stage)
29
+ return drawn.includes(step.stage) || (step.stage === "video" && shortStill.test(reason))
28
30
  ? { kind: "refused", label: "Soften and retry", soften: true }
29
31
  : { kind: "refused", label: "Edit the prompt", soften: false };
30
32
  if (retiredModel.test(reason))
@@ -26,8 +26,12 @@ export function preparationFuture(context, segment, dependency, additionalDepend
26
26
  }, [...new Set([dependency.key, ...additionalDependencies.map((row) => row.key)])]);
27
27
  }
28
28
  export function preparationForGroup(context, logicalKey, source, segment, dependsOn, maxCharacters, spans = [], aliases = [], speaker) {
29
+ // A delivery note for this chunk alone (Edit project → Narration), after the project's prompt.
30
+ const override = context.content.narrationOverrides[logicalKey];
31
+ const note = override?.kind === "text" ? override.direction : undefined;
32
+ const prompt = renderedPrompt(context, "narration");
29
33
  const preparation = recipe(context, `narration:prepare:${segment}:${logicalKey}`, "audio", {
30
- ...llmInput(context, preparationMessages(renderedPrompt(context, "narration"), source, aliases, context.config.language, speaker)),
34
+ ...llmInput(context, preparationMessages(note === undefined ? prompt : `${prompt}\n\n${note}`, source, aliases, context.config.language, speaker)),
31
35
  preparation: { format: "inworld-tts-2", version: 1, source, logicalKey, segment },
32
36
  }, dependsOn, { unresolved: source.trim() === "" });
33
37
  if (source.trim() === "")
@@ -15,6 +15,21 @@ export function refusedKeys(db, projectId, stage) {
15
15
  .all(projectId, stage)
16
16
  .map((row) => String(row.work_key));
17
17
  }
18
+ // A short's still (`shorts:N:image:M`) is drawn in the Video stage, beside steps that draw
19
+ // nothing; only those stills can be softened there.
20
+ const shortStill = /^shorts:\d+:image:\d+$/;
21
+ // The refused steps Soften and retry can reword: every refused image of Images or Thumbnail,
22
+ // and the refused short stills of Video.
23
+ export function softenableKeys(db, projectId, stage) {
24
+ return softenable(stage, refusedKeys(db, projectId, stage));
25
+ }
26
+ export function softenable(stage, refused) {
27
+ if (stage === "images" || stage === "thumbnail")
28
+ return [...refused];
29
+ if (stage === "video")
30
+ return refused.filter((key) => shortStill.test(key));
31
+ return [];
32
+ }
18
33
  export function requestSoftening(db, projectId, keys, at) {
19
34
  for (const key of keys)
20
35
  db.prepare("INSERT INTO prompt_softening(project_id,work_key,requested_at) VALUES (?,?,?) ON CONFLICT(project_id,work_key) DO UPDATE SET requested_at=excluded.requested_at").run(projectId, key, at);
@@ -55,7 +55,13 @@ export const revisionContentSchema = z
55
55
  .strict()),
56
56
  narrationOverrides: z.record(workKey, z.discriminatedUnion("kind", [
57
57
  z.object({ kind: z.literal("asset"), assetId: id }).strict(),
58
- z.object({ kind: z.literal("text"), text: z.string().trim().min(1).max(500000) }).strict(),
58
+ z
59
+ .object({
60
+ kind: z.literal("text"),
61
+ text: z.string().trim().min(1).max(500000),
62
+ direction: z.string().trim().min(1).max(20000).optional(),
63
+ })
64
+ .strict(),
59
65
  ])),
60
66
  narrationSources: z
61
67
  .record(workKey, z