@interware/content-generator 0.7.0 → 0.7.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.
@@ -266,14 +266,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
266
266
  category,
267
267
  threshold: safety ? genai.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : genai.HarmBlockThreshold.BLOCK_ONLY_HIGH
268
268
  }));
269
- const PEOPLE = {
270
- none: "ALLOW_NONE",
271
- adults: genai.PersonGeneration.ALLOW_ADULT
272
- };
269
+ const NO_PEOPLE = "ALLOW_NONE";
273
270
  const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
274
271
  ...aspectRatio ? { aspectRatio } : {},
275
272
  ...imageSize ? { imageSize } : {},
276
- ...backend === "vertex" ? { personGeneration: allowPeople ? PEOPLE.adults : PEOPLE.none } : {}
273
+ ...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
277
274
  });
278
275
  const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
279
276
  text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
@@ -339,6 +336,10 @@ const EXPAND_INSTRUCTION = (concept) => [
339
336
  `Concepto: ${concept}`
340
337
  ].join("\n");
341
338
  class BlockedError extends Error {
339
+ constructor(message, detail) {
340
+ super(message);
341
+ this.detail = detail;
342
+ }
342
343
  }
343
344
  const sourceOf = (chunk) => {
344
345
  if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
@@ -360,12 +361,21 @@ const BLOCK_REASONS = {
360
361
  PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
361
362
  IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
362
363
  };
364
+ const blockMessage = (reason, googleMessage) => {
365
+ const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
366
+ const suffix = code ? ` (Código de Google: ${code})` : "";
367
+ if (/PersonGeneration/i.test(googleMessage)) {
368
+ return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
369
+ }
370
+ return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
371
+ };
363
372
  const imageFrom = (res, model) => {
364
373
  const parts = res.candidates?.[0]?.content?.parts ?? [];
365
374
  const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
366
375
  if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
367
- const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
368
- throw new BlockedError(BLOCK_REASONS[String(reason)] ?? "El modelo no devolvió una imagen. Intenta con otro prompt.");
376
+ const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
377
+ const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
378
+ throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
369
379
  };
370
380
  const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
371
381
  const userTurn = (parts) => [{ role: "user", parts }];
@@ -486,7 +496,10 @@ const imageStudio = ({ strapi }) => {
486
496
  ctx.body = await work();
487
497
  } catch (err) {
488
498
  if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
489
- if (err instanceof BlockedError) return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
499
+ if (err instanceof BlockedError) {
500
+ strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
501
+ return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
502
+ }
490
503
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
491
504
  return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
492
505
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import { promises } from "fs";
3
- import { HarmCategory, PersonGeneration, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
3
+ import { HarmCategory, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
4
4
  const PLUGIN_ID = "strapi-content-generator-gemini";
5
5
  const FOLDER_UID = "plugin::upload.folder";
6
6
  const hasValidPath = (path) => /^(\/\d+)+$/.test(path);
@@ -248,14 +248,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
248
248
  category,
249
249
  threshold: safety ? HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : HarmBlockThreshold.BLOCK_ONLY_HIGH
250
250
  }));
251
- const PEOPLE = {
252
- none: "ALLOW_NONE",
253
- adults: PersonGeneration.ALLOW_ADULT
254
- };
251
+ const NO_PEOPLE = "ALLOW_NONE";
255
252
  const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
256
253
  ...aspectRatio ? { aspectRatio } : {},
257
254
  ...imageSize ? { imageSize } : {},
258
- ...backend === "vertex" ? { personGeneration: allowPeople ? PEOPLE.adults : PEOPLE.none } : {}
255
+ ...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
259
256
  });
260
257
  const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
261
258
  text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
@@ -321,6 +318,10 @@ const EXPAND_INSTRUCTION = (concept) => [
321
318
  `Concepto: ${concept}`
322
319
  ].join("\n");
323
320
  class BlockedError extends Error {
321
+ constructor(message, detail) {
322
+ super(message);
323
+ this.detail = detail;
324
+ }
324
325
  }
325
326
  const sourceOf = (chunk) => {
326
327
  if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
@@ -342,12 +343,21 @@ const BLOCK_REASONS = {
342
343
  PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
343
344
  IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
344
345
  };
346
+ const blockMessage = (reason, googleMessage) => {
347
+ const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
348
+ const suffix = code ? ` (Código de Google: ${code})` : "";
349
+ if (/PersonGeneration/i.test(googleMessage)) {
350
+ return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
351
+ }
352
+ return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
353
+ };
345
354
  const imageFrom = (res, model) => {
346
355
  const parts = res.candidates?.[0]?.content?.parts ?? [];
347
356
  const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
348
357
  if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
349
- const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
350
- throw new BlockedError(BLOCK_REASONS[String(reason)] ?? "El modelo no devolvió una imagen. Intenta con otro prompt.");
358
+ const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
359
+ const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
360
+ throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
351
361
  };
352
362
  const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
353
363
  const userTurn = (parts) => [{ role: "user", parts }];
@@ -468,7 +478,10 @@ const imageStudio = ({ strapi }) => {
468
478
  ctx.body = await work();
469
479
  } catch (err) {
470
480
  if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
471
- if (err instanceof BlockedError) return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
481
+ if (err instanceof BlockedError) {
482
+ strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
483
+ return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
484
+ }
472
485
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
473
486
  return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
474
487
  }
@@ -4,8 +4,13 @@ import type { Core } from '@strapi/strapi';
4
4
  import type { GoogleGenAI } from '@google/genai';
5
5
  import type { Backend } from '../image-studio/geminiConfig';
6
6
  import type { EditInput, GenerateInput } from '../image-studio/validate';
7
- /** El modelo no devolvió imagen (bloqueo de seguridad u otra causa): el controlador responde 422. */
7
+ /**
8
+ * El modelo no devolvió imagen (bloqueo de seguridad u otra causa): el controlador responde 422 con
9
+ * `message` y registra `detail` (motivo y mensaje de Google, con su código de soporte).
10
+ */
8
11
  export declare class BlockedError extends Error {
12
+ readonly detail?: string;
13
+ constructor(message: string, detail?: string);
9
14
  }
10
15
  type ModelsApi = Pick<GoogleGenAI['models'], 'generateContent' | 'list'>;
11
16
  type ReadFile = (path: string) => Promise<Buffer>;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.7.1",
3
3
  "keywords": [],
4
4
  "type": "commonjs",
5
5
  "publishConfig": {