@gentbajko/slopify 3.2.1 → 3.2.3

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,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.
@@ -0,0 +1,21 @@
1
+ # Slopify 3.2.3
2
+
3
+ Released 1 October 2026.
4
+
5
+ 3.2.3 makes Soften and retry actually reword a short's refused picture, and shows a render's progress on Home.
6
+
7
+ ## Shorts
8
+
9
+ - **Soften and retry** on a short's picture now draws the reworded prompt. In 3.2.2 the button appeared, but the picture was asked for with its original wording again and was refused again.
10
+
11
+ ## Home
12
+
13
+ - **Running now** shows what a running step is doing and how far it is ("Rendering the video (45%)"), as the project page does, instead of a step count that stood still through the whole render.
14
+
15
+ ## Thumbnails
16
+
17
+ - Regenerating a thumbnail asks with its own sentence: it makes a new thumbnail and leaves the video and shorts alone. It used the image's wording, which said the video would be rendered again.
18
+
19
+ ## Upgrading from 3.2.2
20
+
21
+ - **Nothing becomes outdated.**
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "id": "3.2.3",
4
+ "title": "Slopify 3.2.3",
5
+ "version": "3.2.3",
6
+ "date": "2026-10-01"
7
+ },
8
+ {
9
+ "id": "3.2.2",
10
+ "title": "Slopify 3.2.2",
11
+ "version": "3.2.2",
12
+ "date": "2026-10-01"
13
+ },
2
14
  {
3
15
  "id": "3.2.1",
4
16
  "title": "Slopify 3.2.1",
@@ -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))
@@ -171,7 +171,8 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
171
171
  }
172
172
  // Soften and retry (`soften.ts`): the project's AI model rewords a refused image prompt
173
173
  // before the image is drawn again. Undefined when nobody asked, or the project has no model.
174
- async function softenIfAsked(deps, context, providers, piece, prompt, label) {
174
+ // A short's stills are drawn in `runtime-shorts.ts`, which asks the same way.
175
+ export async function softenIfAsked(deps, context, providers, piece, prompt, label) {
175
176
  if (!softeningRequested(deps.db, context.work.projectId, piece.key))
176
177
  return undefined;
177
178
  const llm = executionView(deps, context.work.projectId, context.work.revisionId)?.revision.config
@@ -21,8 +21,10 @@ import { figureShots } from "./runtime-export-edit.js";
21
21
  import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
22
22
  import { imageCall } from "./runtime-image.js";
23
23
  import { savedCatalogue } from "./runtime-plan.js";
24
+ import { softenIfAsked } from "./runtime-provider.js";
24
25
  import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
25
26
  import { wordsSchema } from "./runtime-subtitles.js";
27
+ import { clearSoftening } from "./soften.js";
26
28
  // The Shorts step's four kinds of work (`recipe-shorts.ts`): the pick, each clip's image
27
29
  // prompts, each image, and each clip's render. Each is one piece of its own, so a failure
28
30
  // names the clip it belongs to and Try again redoes only what did not finish.
@@ -173,9 +175,14 @@ async function image(deps, context, providers, piece, clip, index) {
173
175
  const input = piece.input;
174
176
  if (input.kind !== "image")
175
177
  throw setupError();
176
- const made = await providers
177
- .forPiece(piece.id)
178
- .image(imageCall(deps, context.work.projectId, input, `Short ${String(clip.number)} image ${String(index)}`));
178
+ const wrapped = providers.forPiece(piece.id);
179
+ const label = `Short ${String(clip.number)} image ${String(index)}`;
180
+ // Soften and retry reworded this still's refused prompt: draw that one instead.
181
+ const softened = await softenIfAsked(deps, context, wrapped, piece, input.prompt, label);
182
+ if (softened === "held")
183
+ return "held";
184
+ const prompt = softened ?? input.prompt;
185
+ const made = await wrapped.image(imageCall(deps, context.work.projectId, { ...input, prompt }, label));
179
186
  if (!made.ok)
180
187
  return "held";
181
188
  const asset = writeAsset(deps, context.work.projectId, made.value.mime === "image/jpeg" ? "short-image.jpg" : "short-image.png", made.value.bytes);
@@ -183,11 +190,13 @@ async function image(deps, context, providers, piece, clip, index) {
183
190
  short: clip.number,
184
191
  sentences: [clip.first, clip.last],
185
192
  index,
186
- prompt: input.prompt,
193
+ prompt,
187
194
  provider: input.provider,
188
195
  model: input.model,
189
196
  });
190
- await publishResult(deps, context, piece, [output], { prompt: input.prompt }, asset);
197
+ await publishResult(deps, context, piece, [output], { prompt }, asset);
198
+ if (softened !== undefined)
199
+ clearSoftening(deps.db, context.work.projectId, [piece.key]);
191
200
  return "done";
192
201
  }
193
202
  async function render(deps, context, piece, snapshot, clip) {
@@ -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);