editorbrain 0.12.0 → 0.18.0
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 +2 -2
- package/bin/editorbrain.mjs +924 -51
- package/package.json +1 -1
- package/skill/editorbrain/AGENTS.md +2 -0
- package/skill/editorbrain/SKILL.md +1 -1
- package/skill/editorbrain/references/cli-mcp.md +9 -0
- package/skill/editorbrain/references/integrations.md +73 -0
- package/skill/editorbrain/references/media-preferences.md +23 -0
package/bin/editorbrain.mjs
CHANGED
|
@@ -200,22 +200,703 @@ function checkStyle(files, id) {
|
|
|
200
200
|
return finish2(documents);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
// packages/core/src/editorial-delivery.ts
|
|
204
|
+
var editorialDeliveryOptions = {
|
|
205
|
+
narration: ["original", "voiceover", "none"],
|
|
206
|
+
onScreenText: ["minimal", "key-points", "detailed"],
|
|
207
|
+
broll: ["footage", "stills", "mixed", "none"],
|
|
208
|
+
captionLanguage: ["source", "brand"],
|
|
209
|
+
captionTreatment: ["clean", "word-highlight", "karaoke", "boxed"],
|
|
210
|
+
captionPosition: ["lower", "center", "adaptive"],
|
|
211
|
+
ctaGoal: ["adaptive", "follow", "comment", "visit", "contact", "none"],
|
|
212
|
+
ctaTreatment: ["adaptive", "end-card", "overlay", "spoken"]
|
|
213
|
+
};
|
|
214
|
+
function editorialDeliveryIssues(value) {
|
|
215
|
+
if (value === void 0) return [];
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return ["editorialDelivery must be an object."];
|
|
217
|
+
const input = value;
|
|
218
|
+
if (Object.keys(input).some((key) => !Object.prototype.hasOwnProperty.call(editorialDeliveryOptions, key))) return [`editorialDelivery supports ${Object.keys(editorialDeliveryOptions).join(", ")} only.`];
|
|
219
|
+
return Object.entries(editorialDeliveryOptions).flatMap(([key, options]) => input[key] === void 0 || typeof input[key] === "string" && options.includes(input[key]) ? [] : [`Invalid editorialDelivery ${key}. Omit undecided choices.`]);
|
|
220
|
+
}
|
|
221
|
+
function resolveEditorialDelivery(value = {}, captionsEnabled) {
|
|
222
|
+
const issues = editorialDeliveryIssues(value);
|
|
223
|
+
if (issues.length) throw new Error(issues.join("\n"));
|
|
224
|
+
const active = Object.fromEntries(Object.entries(value).filter(([, choice]) => choice !== void 0));
|
|
225
|
+
if (captionsEnabled === false) {
|
|
226
|
+
delete active.captionLanguage;
|
|
227
|
+
delete active.captionTreatment;
|
|
228
|
+
delete active.captionPosition;
|
|
229
|
+
}
|
|
230
|
+
if (active.ctaGoal === "none") delete active.ctaTreatment;
|
|
231
|
+
return active;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// packages/core/src/media-styles.ts
|
|
235
|
+
var sourceByStyle = {
|
|
236
|
+
"editorial-photo": { title: "Adobe: Editorial photography", url: "https://www.adobe.com/creativecloud/photography/type/editorial-photography.html" },
|
|
237
|
+
"studio-product": { title: "Adobe: Product photography", url: "https://www.adobe.com/creativecloud/photography/type/product-photography.html" },
|
|
238
|
+
"paper-collage": { title: "Adobe: Collage composition", url: "https://www.adobe.com/products/photoshop/collage.html" },
|
|
239
|
+
"ink-diagram": { title: "Adobe: Line art", url: "https://www.adobe.com/creativecloud/illustration/discover/line-art.html" },
|
|
240
|
+
"soft-3d": { title: "Blender: Rendering", url: "https://www.blender.org/features/rendering/" },
|
|
241
|
+
"flat-vector": { title: "Adobe: Illustration", url: "https://www.adobe.com/creativecloud/illustration.html" }
|
|
242
|
+
};
|
|
243
|
+
var brandAdaptation = "Los tokens de marca gobiernan colores y tipograf\xEDas de los gr\xE1ficos. Conserva los colores aut\xE9nticos del material, producto y logos; no heredes la paleta de la muestra.";
|
|
244
|
+
var expansionSources = {
|
|
245
|
+
"cinematic-scene": [
|
|
246
|
+
{ title: "Adobe: Cinematic lighting and depth", url: "https://helpx.adobe.com/ee/photoshop/how-to/standout-photos-lighting-vignette.html" },
|
|
247
|
+
{ title: "Referencia visual: luis_.hermida \xB7 5.0 s", url: "https://www.instagram.com/p/DY0ZmQevH2J/" }
|
|
248
|
+
],
|
|
249
|
+
"surreal-metaphor": [
|
|
250
|
+
{ title: "Adobe: Surreal photography", url: "https://www.adobe.com/uk/creativecloud/photography/discover/surreal-photography.html" },
|
|
251
|
+
{ title: "Referencia visual: russ.lockedin \xB7 19.0 y 28.5 s", url: "https://www.instagram.com/p/DbtWJOztrTO/" }
|
|
252
|
+
],
|
|
253
|
+
"painterly-story": [
|
|
254
|
+
{ title: "Adobe: Visual appearance of oil painting", url: "https://helpx.adobe.com/in/photoshop/using/oil-paint-filter.html" },
|
|
255
|
+
{ title: "Referencia visual: russ.lockedin \xB7 14.5 y 22.5 s", url: "https://www.instagram.com/p/DbWkY9ZSXtQ/" }
|
|
256
|
+
],
|
|
257
|
+
"vintage-engraving": [
|
|
258
|
+
{ title: "Adobe: Line, hatching and volume", url: "https://www.adobe.com/creativecloud/illustration/discover/line-art.html" },
|
|
259
|
+
{ title: "Referencia visual: russ.lockedin \xB7 17.5 s", url: "https://www.instagram.com/p/DbWkY9ZSXtQ/" }
|
|
260
|
+
],
|
|
261
|
+
"riso-playful": [
|
|
262
|
+
{ title: "RISO: Creative print, color and texture", url: "https://www.riso.co.uk/print-solutions/risograph-creative/" },
|
|
263
|
+
{ title: "Referencia de ilustraci\xF3n na\xEFf: ryanbailey.cb \xB7 11.5 s", url: "https://www.instagram.com/p/DbZDnSpyilk/" }
|
|
264
|
+
],
|
|
265
|
+
"sensory-macro": [
|
|
266
|
+
{ title: "Adobe: Macro photography", url: "https://www.adobe.com/creativecloud/photography/discover/macro-photography.html" },
|
|
267
|
+
{ title: "Referencia visual: ryanbailey.cb \xB7 15.5 y 24.5 s", url: "https://www.instagram.com/p/DbZDnSpyilk/" }
|
|
268
|
+
]
|
|
269
|
+
};
|
|
270
|
+
var image = (id, name, description, purpose, recipe, parameters, requirements, avoidWhen) => ({
|
|
271
|
+
id,
|
|
272
|
+
version: "1.0.0",
|
|
273
|
+
category: "image",
|
|
274
|
+
name,
|
|
275
|
+
description,
|
|
276
|
+
purpose,
|
|
277
|
+
recipe,
|
|
278
|
+
parameters,
|
|
279
|
+
adaptationRules: [brandAdaptation, "Adapta sujeto, encuadre y espacio para texto al contenido real y al formato. Usa recursos autorizados; una preferencia permite proponer, no autoriza generaci\xF3n ni compras."],
|
|
280
|
+
requirements,
|
|
281
|
+
avoidWhen,
|
|
282
|
+
status: "concept",
|
|
283
|
+
sources: [sourceByStyle[id]],
|
|
284
|
+
preview: { kind: "illustration", description: "Muestras originales generadas con IA del tratamiento visual; no acreditan exactitud factual, un render de cliente ni una transferencia probada. Vector limpio es una apariencia raster, no un archivo SVG editable.", examples: ["square", "portrait", "landscape"].map((format, index) => ({ ratio: ["1:1", "9:16", "16:9"][index], url: `https://editorbrain.com/media-styles/v1/${id}-${format}.webp`, provenance: "ai-generated" })) }
|
|
285
|
+
});
|
|
286
|
+
var expandedImage = (...args) => {
|
|
287
|
+
const style = image(...args);
|
|
288
|
+
return {
|
|
289
|
+
...style,
|
|
290
|
+
sources: expansionSources[style.id],
|
|
291
|
+
adaptationRules: [...style.adaptationRules, "Distingue hechos, reconstrucci\xF3n e imagen metaf\xF3rica: el tratamiento no acredita un evento, resultado o propiedad real. Los par\xE1metros son decisiones editoriales adaptables, no mediciones del referente."],
|
|
292
|
+
preview: {
|
|
293
|
+
kind: "illustration",
|
|
294
|
+
description: "Escenas originales generadas con IA, inspiradas en tratamientos observados. Son muestras de apariencia, no copias del referente, documentos hist\xF3ricos ni prueba de t\xE9cnica f\xEDsica, movimiento o sonido.",
|
|
295
|
+
examples: ["square", "portrait", "landscape"].map((format, index) => ({ ratio: ["1:1", "9:16", "16:9"][index], url: `https://editorbrain.com/media-styles/v2/${style.id}-${format}.webp`, provenance: "ai-generated" }))
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
var motion = (id, name, description, purpose, recipe, parameters, requirements, avoidWhen) => ({
|
|
300
|
+
id,
|
|
301
|
+
version: "1.0.0",
|
|
302
|
+
category: "motion",
|
|
303
|
+
name,
|
|
304
|
+
description,
|
|
305
|
+
purpose,
|
|
306
|
+
recipe,
|
|
307
|
+
parameters,
|
|
308
|
+
adaptationRules: [brandAdaptation, "Los tiempos son puntos de partida: ajusta a voz, acci\xF3n, lectura, resoluci\xF3n y fps reales. Con movimiento reducido, usa cortes o estados est\xE1ticos legibles. No deduzcas ni a\xF1adas SFX por esta animaci\xF3n."],
|
|
309
|
+
requirements,
|
|
310
|
+
avoidWhen,
|
|
311
|
+
status: "concept",
|
|
312
|
+
sources: [{ title: "Adobe: Six principles of animation in UX", url: "https://blog.adobe.com/en/publish/2019/06/19/designing-animation-six-principles-using-animation-ux" }],
|
|
313
|
+
preview: { kind: "animation", description: "Demostraci\xF3n del mecanismo temporal; no es una edici\xF3n de cliente ni prueba de calidad de transferencia." }
|
|
314
|
+
});
|
|
315
|
+
var composition = (args, source, formats) => {
|
|
316
|
+
const style = motion(...args);
|
|
317
|
+
return {
|
|
318
|
+
...style,
|
|
319
|
+
sources: [source],
|
|
320
|
+
adaptationRules: [
|
|
321
|
+
...style.adaptationRules,
|
|
322
|
+
"Los par\xE1metros propuestos son decisiones de adaptaci\xF3n, no tiempos ni movimientos medidos del referente. Conserva restricciones de recursos, formato de b-roll y permisos; elegir este dise\xF1o no autoriza generaci\xF3n ni gasto.",
|
|
323
|
+
...formats,
|
|
324
|
+
"Texto, cifras y anotaciones exactas permanecen editables en c\xF3digo. Una imagen generada no prueba un hecho; verifica continuidad, recortes y legibilidad con los recursos reales antes del render."
|
|
325
|
+
],
|
|
326
|
+
preview: { kind: "animation", description: "Composici\xF3n original con recursos ilustrativos y capas de c\xF3digo. La muestra demuestra una propuesta de organizaci\xF3n y secuencia; no reproduce al referente ni certifica hechos, sonido, herramientas disponibles o transferencia al material del cliente." }
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
var MEDIA_STYLE_CATALOG = {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
id: "editorbrain-media-styles",
|
|
332
|
+
version: "1.4.0",
|
|
333
|
+
styles: [
|
|
334
|
+
image(
|
|
335
|
+
"editorial-photo",
|
|
336
|
+
"Fotograf\xEDa editorial",
|
|
337
|
+
"Luz natural, un sujeto claro y contexto real.",
|
|
338
|
+
"Aportar contexto humano y credibilidad sin distraer del mensaje.",
|
|
339
|
+
["Define una acci\xF3n o sujeto concreto relacionado con el beat.", "Comp\xF3n un plano principal con profundidad suave y fondo contextual.", "Reserva espacio libre solo donde habr\xE1 texto; conserva textura y tonos naturales."],
|
|
340
|
+
[{ key: "subject_share", value: [35, 65], unit: "% del encuadre", adaptation: "Ajusta al objeto y al recorte final; no cortes manos o informaci\xF3n relevante." }, { key: "light", value: "Una fuente amplia lateral, sombras suaves", adaptation: "Respeta la luz existente si usas fotograf\xEDa real." }],
|
|
341
|
+
["Fotograf\xEDa propia/licenciada o herramienta de generaci\xF3n autorizada.", "Sujeto y contexto definidos por el proyecto."],
|
|
342
|
+
["Se requiere mostrar con exactitud un evento o persona y solo hay material sint\xE9tico.", "La escena necesita demostrar datos o relaciones abstractas."]
|
|
343
|
+
),
|
|
344
|
+
image(
|
|
345
|
+
"studio-product",
|
|
346
|
+
"Producto de estudio",
|
|
347
|
+
"Un producto protagonista con separaci\xF3n limpia del fondo.",
|
|
348
|
+
"Mostrar forma, material y detalles relevantes de un producto.",
|
|
349
|
+
["Parte del producto real y conserva envase, etiquetas y proporciones.", "Usa una base simple y luz suave que separe los contornos.", "A\xF1ade como m\xE1ximo un detalle contextual que ayude a explicar el beneficio."],
|
|
350
|
+
[{ key: "product_share", value: [45, 75], unit: "% de altura", adaptation: "Deja margen para el recorte y la informaci\xF3n del producto." }, { key: "background", value: "Superficie simple con sombra de contacto", adaptation: "Usa roles de marca; no inventes materiales o claims." }],
|
|
351
|
+
["Imagen precisa del producto y permisos de uso.", "Recorte o separaci\xF3n de sujeto si se cambia el fondo."],
|
|
352
|
+
["No hay referencia fiable del producto.", "El acabado estilizado oculta caracter\xEDsticas importantes."]
|
|
353
|
+
),
|
|
354
|
+
image(
|
|
355
|
+
"paper-collage",
|
|
356
|
+
"Collage de papel",
|
|
357
|
+
"Recortes, capas y textura discreta con jerarqu\xEDa editorial.",
|
|
358
|
+
"Relacionar ideas o fuentes distintas mediante una composici\xF3n tangible.",
|
|
359
|
+
["Elige un elemento principal y hasta dos apoyos.", "Recorta siluetas y combina bordes rectos con un detalle de papel.", "Separa capas con sombra sutil; mantiene el texto fuera de las zonas con textura."],
|
|
360
|
+
[{ key: "support_layers", value: [1, 3], unit: "capas", adaptation: "Reduce las capas si el formato o el texto pierden legibilidad." }, { key: "rotation", value: [-4, 4], unit: "grados", adaptation: "No gires documentos o etiquetas cuya lectura sea esencial." }],
|
|
361
|
+
["Im\xE1genes recortables con derechos y suficiente resoluci\xF3n.", "Capas editables para composici\xF3n."],
|
|
362
|
+
["Hay muchos datos densos que deben leerse.", "La marca necesita una presentaci\xF3n fotogr\xE1fica sin intervenci\xF3n."]
|
|
363
|
+
),
|
|
364
|
+
image(
|
|
365
|
+
"ink-diagram",
|
|
366
|
+
"Diagrama de tinta",
|
|
367
|
+
"Trazos claros, etiquetas breves y relaciones visibles.",
|
|
368
|
+
"Explicar un proceso, contraste o relaci\xF3n que la fotograf\xEDa no muestra.",
|
|
369
|
+
["Reduce el concepto a nodos y relaciones verificables.", "Dibuja trazos consistentes y flechas solo con direcci\xF3n significativa.", "Usa etiquetas breves; diferencia el acento sem\xE1ntico del trazo principal."],
|
|
370
|
+
[{ key: "nodes", value: [2, 5], unit: "nodos por estado", adaptation: "Divide un sistema mayor en varios estados." }, { key: "stroke", value: [2, 4], unit: "px a 1080 de ancho", adaptation: "Escala con la salida y comprueba visibilidad en m\xF3vil." }],
|
|
371
|
+
["Contenido y relaciones confirmados por el guion.", "Texto editable y motor de dibujo o vector."],
|
|
372
|
+
["La relaci\xF3n a\xFAn es una hip\xF3tesis no etiquetada.", "La simplificaci\xF3n elimina informaci\xF3n necesaria."]
|
|
373
|
+
),
|
|
374
|
+
image(
|
|
375
|
+
"soft-3d",
|
|
376
|
+
"Volumen suave",
|
|
377
|
+
"Objetos simples con profundidad, luz amplia y materiales mates.",
|
|
378
|
+
"Dar forma a conceptos abstractos mediante met\xE1foras visuales sencillas.",
|
|
379
|
+
["Elige una met\xE1fora concreta y comprensible para el concepto.", "Modela pocas formas con bordes suaves y materiales mates.", "Usa una c\xE1mara estable, sombra de contacto y contraste legible."],
|
|
380
|
+
[{ key: "primary_objects", value: [1, 3], unit: "objetos", adaptation: "Prioriza una relaci\xF3n clara frente a decoraci\xF3n." }, { key: "material", value: "Mate, rugosidad media-alta", adaptation: "Si el objeto representa un producto real, respeta su material." }],
|
|
381
|
+
["Motor 3D o herramienta de generaci\xF3n autorizada.", "Met\xE1fora que no confunda con evidencia de un producto real."],
|
|
382
|
+
["Se necesita exactitud documental.", "La met\xE1fora introduce una promesa o caracter\xEDstica no confirmada."]
|
|
383
|
+
),
|
|
384
|
+
image(
|
|
385
|
+
"flat-vector",
|
|
386
|
+
"Vector limpio",
|
|
387
|
+
"Formas planas, contornos precisos y contraste sem\xE1ntico.",
|
|
388
|
+
"Explicar categor\xEDas o pasos con gr\xE1ficos claros y reutilizables.",
|
|
389
|
+
["Construye siluetas simples y consistentes.", "Limita cada panel a una acci\xF3n y un foco visual.", "Usa rellenos sem\xE1nticos de marca y texto editable separado."],
|
|
390
|
+
[{ key: "visual_roles", value: [2, 4], unit: "roles por panel", adaptation: "Los roles se asignan a tokens de marca; no fijan colores nuevos." }, { key: "detail", value: "Sin textura ni microdetalle decorativo", adaptation: "Conserva detalles que distinguen funciones o categor\xEDas." }],
|
|
391
|
+
["Motor vectorial o composici\xF3n con formas.", "Etiquetas y conceptos definidos."],
|
|
392
|
+
["La fotograf\xEDa del objeto real es necesaria para reconocerlo.", "Las diferencias importantes dependen de textura o acabado."]
|
|
393
|
+
),
|
|
394
|
+
expandedImage(
|
|
395
|
+
"cinematic-scene",
|
|
396
|
+
"Cine atmosf\xE9rico",
|
|
397
|
+
"Luz dirigida, profundidad y una escena que concentra la emoci\xF3n.",
|
|
398
|
+
"Dar contexto emocional a una acci\xF3n o decisi\xF3n del guion.",
|
|
399
|
+
["Define una acci\xF3n legible y el estado emocional que debe acompa\xF1ar.", "Comp\xF3n sujeto y entorno con una fuente de luz coherente y separaci\xF3n de planos.", "Conserva detalle en las sombras relevantes; ajusta el encuadre a cada formato sin copiar personajes del referente."],
|
|
400
|
+
[{ key: "focal_actions", value: 1, unit: "acci\xF3n principal", adaptation: "Puede ser un gesto, una espera o un descubrimiento; debe corresponder al beat." }, { key: "light", value: "Una fuente dominante y relleno seg\xFAn legibilidad", adaptation: "El contraste responde a la escena; no impongas \xE1mbar ni una LUT a material aut\xE9ntico." }, { key: "depth", value: "Sujeto, contexto y fondo diferenciados", adaptation: "Mant\xE9n enfocada la informaci\xF3n necesaria para comprender la acci\xF3n." }],
|
|
401
|
+
["Acci\xF3n y contexto definidos; recursos propios o autorizados.", "Referencia fiel cuando se represente una persona o producto real."],
|
|
402
|
+
["El dramatismo cambia el sentido de un testimonio.", "Una escena inventada podr\xEDa confundirse con evidencia del evento descrito."]
|
|
403
|
+
),
|
|
404
|
+
expandedImage(
|
|
405
|
+
"surreal-metaphor",
|
|
406
|
+
"Met\xE1fora surrealista",
|
|
407
|
+
"Una relaci\xF3n imposible convierte una idea abstracta en una escena legible.",
|
|
408
|
+
"Representar tensi\xF3n, escala o cambio mediante una met\xE1fora vinculada al guion.",
|
|
409
|
+
["Reduce el concepto a una relaci\xF3n visual comprensible.", "Introduce una sola contradicci\xF3n de escala, material o espacio en una escena coherente.", "Mant\xE9n luz y perspectiva consistentes; distingue la met\xE1fora de una afirmaci\xF3n factual."],
|
|
410
|
+
[{ key: "visual_contradictions", value: 1, unit: "relaci\xF3n central", adaptation: "Omite s\xEDmbolos adicionales que necesiten explicaci\xF3n o cambien el sentido." }, { key: "scale_anchor", value: "Un objeto o sujeto de escala reconocible", adaptation: "El ancla debe ayudar a entender la relaci\xF3n imposible sin obligar a incluir una persona." }, { key: "scene_continuity", value: "Luz y perspectiva compartidas", adaptation: "La imposibilidad es conceptual; evita errores de composici\xF3n que parezcan accidentales." }],
|
|
411
|
+
["Concepto claro y met\xE1fora adecuada al p\xFAblico.", "Composici\xF3n o generaci\xF3n autorizada de una escena nueva."],
|
|
412
|
+
["La imagen puede sugerir una capacidad real o resultado no demostrado.", "El contenido requiere mostrar hechos con exactitud documental."]
|
|
413
|
+
),
|
|
414
|
+
expandedImage(
|
|
415
|
+
"painterly-story",
|
|
416
|
+
"Pintura narrativa",
|
|
417
|
+
"Pincelada visible, masas de color y una historia contenida en la escena.",
|
|
418
|
+
"Acompa\xF1ar relatos, recuerdos o ideas con una interpretaci\xF3n pict\xF3rica.",
|
|
419
|
+
["Elige un momento y las relaciones entre sujeto y entorno.", "Construye luz y volumen con masas de color y bordes de distinta definici\xF3n.", "Reserva el detalle para el foco narrativo y deja pincelada visible sin convertirla en ruido."],
|
|
420
|
+
[{ key: "focal_groups", value: [1, 3], unit: "grupos visuales", adaptation: "Reduce personajes y objetos si se pierde la historia en m\xF3vil." }, { key: "brushwork", value: "Visible en formas y transiciones de luz", adaptation: "La escala de pincel responde a la salida; no ocultes rasgos relevantes de un objeto real." }, { key: "edge_hierarchy", value: "Foco definido y bordes secundarios m\xE1s suaves", adaptation: "Mant\xE9n reconocible aquello que sostiene la acci\xF3n." }],
|
|
421
|
+
["Escena original o materiales cuyo uso est\xE9 autorizado.", "Una funci\xF3n narrativa que se beneficie de interpretaci\xF3n visual."],
|
|
422
|
+
["Se necesita probar un detalle hist\xF3rico, m\xE9dico o t\xE9cnico.", "La estilizaci\xF3n cambia identidad o caracter\xEDsticas esenciales."]
|
|
423
|
+
),
|
|
424
|
+
expandedImage(
|
|
425
|
+
"vintage-engraving",
|
|
426
|
+
"Grabado editorial",
|
|
427
|
+
"Rayado, contornos y papel aparente con volumen de estampa hist\xF3rica.",
|
|
428
|
+
"Dar car\xE1cter y profundidad a una escena u objeto ilustrado.",
|
|
429
|
+
["Comp\xF3n una escena u objeto con silueta reconocible.", "Construye luces y sombras mediante l\xEDneas y rayado que sigan el volumen.", "Aplica una paleta corta y textura discreta; mant\xE9n etiquetas y datos verificables separados."],
|
|
430
|
+
[{ key: "ink_roles", value: [1, 3], unit: "roles de tinta aparentes", adaptation: "Asigna los roles a la marca; el aspecto hist\xF3rico no obliga a usar sepia." }, { key: "hatching", value: "Densidad mayor en sombra y menor en luz", adaptation: "Ampl\xEDa separaci\xF3n de l\xEDneas para evitar empastes o moir\xE9 en la salida final." }, { key: "paper_texture", value: "Secundaria respecto a contorno y volumen", adaptation: "Reduce textura donde compita con informaci\xF3n necesaria." }],
|
|
431
|
+
["Ilustraci\xF3n original o recurso autorizado con resoluci\xF3n suficiente.", "Verificaci\xF3n independiente si se atribuye una identidad o hecho hist\xF3rico."],
|
|
432
|
+
["La imagen sint\xE9tica podr\xEDa presentarse como documento de archivo aut\xE9ntico.", "El rayado oculta detalles que deben leerse con precisi\xF3n."]
|
|
433
|
+
),
|
|
434
|
+
expandedImage(
|
|
435
|
+
"riso-playful",
|
|
436
|
+
"Estampa artesanal",
|
|
437
|
+
"Formas expresivas, pocas tintas aparentes y textura de impresi\xF3n.",
|
|
438
|
+
"Aportar calidez y personalidad a una ilustraci\xF3n clara.",
|
|
439
|
+
["Reduce la escena a siluetas expresivas con un foco principal.", "Asigna pocos roles de color y construye cada forma con bordes ligeramente imperfectos.", "A\xF1ade textura de tinta y, si ayuda, un desregistro sutil; comprueba que el contorno conserve lectura."],
|
|
440
|
+
[{ key: "ink_roles", value: [2, 4], unit: "roles de color", adaptation: "Usa tokens compatibles con la marca y suficiente contraste; no heredes la paleta de la muestra." }, { key: "registration", value: "Ligera irregularidad opcional en bordes secundarios", adaptation: "No desplazar letras, logos o detalles que necesitan precisi\xF3n." }, { key: "texture", value: "Grano irregular visible sin cubrir la forma", adaptation: "La textura debe sobrevivir la compresi\xF3n sin convertir \xE1reas planas en ruido." }],
|
|
441
|
+
["Ilustraci\xF3n propia o autorizada y jerarqu\xEDa visual definida.", "Texto y logos exactos en capas independientes si se necesitan."],
|
|
442
|
+
["Se requiere una reproducci\xF3n fiel del acabado de un producto.", "El tono o la lectura exigen geometr\xEDa y contornos completamente precisos."]
|
|
443
|
+
),
|
|
444
|
+
expandedImage(
|
|
445
|
+
"sensory-macro",
|
|
446
|
+
"Detalle sensorial",
|
|
447
|
+
"Materia, luz pr\xF3xima y texturas que ocupan el encuadre.",
|
|
448
|
+
"Hacer visible una cualidad material relevante mediante un detalle cercano.",
|
|
449
|
+
["Elige una textura real o una materia relacionada con el beat.", "Acerca el encuadre y dirige la luz para revelar relieve, gotas, fibras o superficies.", "Mant\xE9n suficiente profundidad de foco para reconocer el detalle y recomp\xF3n para cada proporci\xF3n."],
|
|
450
|
+
[{ key: "material_share", value: [65, 95], unit: "% del encuadre", adaptation: "Deja contexto cuando haga falta reconocer la materia; no recortes un rasgo esencial." }, { key: "focus", value: "Plano de textura principal legible", adaptation: "La profundidad debe revelar la cualidad buscada, no disimular errores." }, { key: "light", value: "Rasante o especular seg\xFAn la superficie", adaptation: "Conserva el color y acabado aut\xE9nticos si la imagen representa un producto real." }],
|
|
451
|
+
["Materia o detalle definido y recursos propios o autorizados.", "Referencia exacta cuando se anuncie una propiedad del producto."],
|
|
452
|
+
["El detalle exagera calidad, composici\xF3n o resultados no demostrados.", "El encuadre impide identificar el objeto que el guion necesita mostrar."]
|
|
453
|
+
),
|
|
454
|
+
motion(
|
|
455
|
+
"quiet-reveal",
|
|
456
|
+
"Entrada sutil",
|
|
457
|
+
"Aparici\xF3n breve con desplazamiento corto y lectura estable.",
|
|
458
|
+
"Introducir informaci\xF3n sin competir con la voz o el sujeto.",
|
|
459
|
+
["Alinea la aparici\xF3n con la frase que presenta la informaci\xF3n.", "Combina opacidad de 0 a 1 con un desplazamiento corto.", "Mant\xE9n el estado final el tiempo que requiere la lectura; retira cuando deja de ser \xFAtil."],
|
|
460
|
+
[{ key: "entry_duration", value: [180, 320], unit: "ms", adaptation: "Ajusta a fps y velocidad de lectura." }, { key: "travel", value: [1, 3], unit: "% de altura", adaptation: "Reduce a cero cuando el sujeto o el fondo ya se mueven." }, { key: "easing", value: "ease-out sin rebote", adaptation: "La llegada debe estabilizarse antes de leer." }],
|
|
461
|
+
["Texto o gr\xE1fico en capa independiente.", "Ancla temporal en voz o acci\xF3n."],
|
|
462
|
+
["La informaci\xF3n ya est\xE1 visible y no necesita \xE9nfasis.", "La entrada retrasa una instrucci\xF3n urgente."]
|
|
463
|
+
),
|
|
464
|
+
motion(
|
|
465
|
+
"kinetic-type",
|
|
466
|
+
"Tipograf\xEDa r\xEDtmica",
|
|
467
|
+
"\xC9nfasis de palabras con ritmo ligado al habla.",
|
|
468
|
+
"Destacar una frase clave sin convertir todas las palabras en est\xEDmulos.",
|
|
469
|
+
["Selecciona una frase y las pocas palabras que realmente cambian su sentido.", "Sincroniza sus estados con la voz verificada.", "Usa un \xFAnico mecanismo de \xE9nfasis y conserva contexto legible."],
|
|
470
|
+
[{ key: "emphasis_scale", value: [1.04, 1.12], unit: "factor", adaptation: "Mant\xE9n la palabra dentro del \xE1rea segura." }, { key: "emphasis_duration", value: [120, 240], unit: "ms", adaptation: "Ajusta al fonema/frase real; no inventes alineaci\xF3n desde un resumen ASR." }, { key: "emphasis_count", value: [1, 3], unit: "palabras por frase", adaptation: "Puede ser cero si el \xE9nfasis no aporta sentido." }],
|
|
471
|
+
["Texto confirmado y fuente disponible.", "Alineaci\xF3n temporal real de la frase o revisi\xF3n manual."],
|
|
472
|
+
["El texto contiene demasiadas palabras para leer.", "El \xE9nfasis cambia el sentido de una cita o declaraci\xF3n."]
|
|
473
|
+
),
|
|
474
|
+
motion(
|
|
475
|
+
"spring-callout",
|
|
476
|
+
"\xC9nfasis el\xE1stico",
|
|
477
|
+
"Una etiqueta llega con un rebote corto y controlado.",
|
|
478
|
+
"Se\xF1alar un detalle puntual de forma expresiva.",
|
|
479
|
+
["Ancla la etiqueta al detalle que se est\xE1 explicando.", "Lleva escala y posici\xF3n al destino con un \xFAnico sobrepaso leve.", "Estabiliza la etiqueta; no mantengas vibraci\xF3n durante la lectura."],
|
|
480
|
+
[{ key: "overshoot", value: [2, 5], unit: "%", adaptation: "Reduce el rebote en marcas sobrias o contenido sensible." }, { key: "settle_duration", value: [250, 450], unit: "ms", adaptation: "Verifica el movimiento con playback al fps final." }],
|
|
481
|
+
["Elemento o punto que justifica el \xE9nfasis.", "Motor con keyframes o resorte controlable."],
|
|
482
|
+
["Compite con una acci\xF3n importante.", "El tono requiere calma o la audiencia necesita movimiento reducido."]
|
|
483
|
+
),
|
|
484
|
+
motion(
|
|
485
|
+
"diagram-build",
|
|
486
|
+
"Diagrama progresivo",
|
|
487
|
+
"Las relaciones aparecen en el orden de la explicaci\xF3n.",
|
|
488
|
+
"Permitir entender un sistema una relaci\xF3n a la vez.",
|
|
489
|
+
["Establece el primer nodo como estado legible.", "Revela cada relaci\xF3n despu\xE9s de mencionar su causa o funci\xF3n.", "Mant\xE9n el conjunto al final para que pueda revisarse."],
|
|
490
|
+
[{ key: "segment_reveal", value: [250, 500], unit: "ms", adaptation: "Usa voz o lectura como ancla; evita un ritmo autom\xE1tico uniforme." }, { key: "active_relation", value: 1, unit: "relaci\xF3n a la vez", adaptation: "Agrupa solo relaciones que se explican conjuntamente." }],
|
|
491
|
+
["Diagrama v\xE1lido con nodos y trazos separados.", "Orden de explicaci\xF3n definido."],
|
|
492
|
+
["La progresi\xF3n implicar\xEDa causalidad no demostrada.", "No hay tiempo suficiente para leer el estado final."]
|
|
493
|
+
),
|
|
494
|
+
motion(
|
|
495
|
+
"match-cut",
|
|
496
|
+
"Corte por correspondencia",
|
|
497
|
+
"Dos planos se conectan por forma, posici\xF3n o acci\xF3n.",
|
|
498
|
+
"Dar continuidad a un cambio de contexto o reforzar una comparaci\xF3n.",
|
|
499
|
+
["Encuentra una correspondencia real entre dos planos autorizados.", "Alinea el foco y el momento de acci\xF3n sin deformar el sujeto.", "Prueba primero un corte directo; comprueba ambos lados a velocidad real."],
|
|
500
|
+
[{ key: "transition_overlap", value: 0, unit: "frames de partida", adaptation: "A\xF1ade solape solo si tiene funci\xF3n editorial; no hace falta fundido." }, { key: "alignment", value: "Punto de atenci\xF3n, forma o direcci\xF3n de acci\xF3n", adaptation: "Elige una correspondencia; no fuerces todas a la vez." }],
|
|
501
|
+
["Dos planos con correspondencia verificable.", "Material suficiente alrededor del punto de corte."],
|
|
502
|
+
["La comparaci\xF3n inventa continuidad factual.", "Los recortes necesarios da\xF1an el contenido."]
|
|
503
|
+
),
|
|
504
|
+
motion(
|
|
505
|
+
"parallax-depth",
|
|
506
|
+
"Profundidad por capas",
|
|
507
|
+
"Desplazamiento suave entre planos de una imagen.",
|
|
508
|
+
"A\xF1adir recorrido visual a una imagen est\xE1tica cuando aporta atenci\xF3n.",
|
|
509
|
+
["Separa sujeto y fondo con bordes limpios.", "Extiende el fondo solo donde sea necesario y est\xE9 autorizado.", "Mueve las capas con velocidades ligeramente distintas sin revelar huecos."],
|
|
510
|
+
[{ key: "layer_travel", value: [1, 3], unit: "% de ancho", adaptation: "Reduce desplazamiento seg\xFAn recorte, bordes y \xE1rea segura." }, { key: "shot_duration", value: [2, 5], unit: "s de partida", adaptation: "Depende del beat y la lectura; una imagen puede permanecer inm\xF3vil." }],
|
|
511
|
+
["Imagen con resoluci\xF3n y capas separables.", "Fondo disponible o reconstrucci\xF3n autorizada."],
|
|
512
|
+
["Se deforman rostros, logos o detalles del producto.", "El movimiento crea una perspectiva imposible o distrae."]
|
|
513
|
+
),
|
|
514
|
+
motion(
|
|
515
|
+
"workflow-scene",
|
|
516
|
+
"Flujo de trabajo",
|
|
517
|
+
"Una escena completa conecta solicitud, agente y resultado.",
|
|
518
|
+
"Explicar c\xF3mo avanza una tarea entre actores y estados mediante una secuencia visible.",
|
|
519
|
+
["Define entradas, responsables, decisiones y resultados que correspondan al proceso real.", "Comp\xF3n la escena en capas editables de JavaScript, HTML/SVG o el motor disponible; separa nodos, conexiones y etiquetas.", "Representa una solicitud recorriendo el flujo; cambia cada estado cuando la narraci\xF3n explica su funci\xF3n.", "Sost\xE9n el resultado y comprueba que el recorrido sea legible en el formato final; una simulaci\xF3n no demuestra una automatizaci\xF3n ejecutada."],
|
|
520
|
+
[{ key: "visible_stages", value: [3, 5], unit: "estados por escena", adaptation: "Divide procesos mayores y conserva bifurcaciones necesarias; no inventes que todo flujo termina en un correo." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Ajusta al contenido, voz, lectura y fps; el ritmo de la demo no mide la duraci\xF3n real del proceso." }, { key: "active_transition", value: 1, unit: "paso de atenci\xF3n", adaptation: "Permite ramas paralelas solo cuando el guion las explica; mant\xE9n etiquetas y valores exactos." }],
|
|
521
|
+
["Proceso verificado y etiquetas editables.", "Herramienta de composici\xF3n y render disponible en la sesi\xF3n; el cat\xE1logo no instala ni acredita un motor."],
|
|
522
|
+
["La simplificaci\xF3n inventa capacidades o resultados de un sistema.", "El proceso exige detalles que no se pueden leer en la duraci\xF3n disponible."]
|
|
523
|
+
),
|
|
524
|
+
motion(
|
|
525
|
+
"product-assembly",
|
|
526
|
+
"Ensamble de producto",
|
|
527
|
+
"Un producto se separa en piezas y vuelve a formar una unidad.",
|
|
528
|
+
"Explicar construcci\xF3n, relaci\xF3n entre partes o un detalle de dise\xF1o en una escena continua.",
|
|
529
|
+
["Identifica las piezas visibles con material del producto real o define un objeto ficticio claramente ilustrativo.", "Prepara capas 2D o geometr\xEDa 3D seg\xFAn los recursos y el motor de JavaScript disponible.", "Separa piezas manteniendo sus relaciones espaciales y re\xFAne el conjunto con un orden comprensible.", "Revisa encajes, oclusiones, r\xF3tulos y estado final; no presentes un despiece imaginado como ingenier\xEDa real."],
|
|
530
|
+
[{ key: "visible_parts", value: [3, 6], unit: "piezas principales", adaptation: "Incluye solo piezas justificadas por la explicaci\xF3n y evidencia; agrupa el resto." }, { key: "separation", value: [5, 16], unit: "% del encuadre", adaptation: "Adapta distancias al producto y evita que las partes crucen \xE1reas seguras o pierdan su relaci\xF3n." }, { key: "settle_duration", value: [500, 900], unit: "ms de partida", adaptation: "Ajusta al tama\xF1o y al relato; comprueba la lectura del conjunto a fps final." }],
|
|
531
|
+
["Piezas autorizadas o modelo original y referencias del producto cuando se afirmen caracter\xEDsticas reales.", "Motor local verificado para las transformaciones 2D/3D elegidas."],
|
|
532
|
+
["No se conoce la construcci\xF3n real y el despiece podr\xEDa inducir a error.", "El movimiento de piezas oculta medidas, etiquetas o detalles necesarios."]
|
|
533
|
+
),
|
|
534
|
+
motion(
|
|
535
|
+
"interface-story",
|
|
536
|
+
"Interfaz en acci\xF3n",
|
|
537
|
+
"Una miniapp muestra una acci\xF3n y el cambio de estado que produce.",
|
|
538
|
+
"Explicar una interacci\xF3n mediante una interfaz breve con texto y estados controlables.",
|
|
539
|
+
["Define la tarea, el estado inicial y el resultado verificable; distingue demo conceptual de captura real.", "Construye una interfaz propia con texto, componentes y estados editables en JavaScript.", "Anima una acci\xF3n clara y su respuesta, manteniendo continuidad espacial entre ambos estados.", "Deja leer el resultado y verifica que la transici\xF3n no invente velocidad, m\xE9tricas o funciones del producto."],
|
|
540
|
+
[{ key: "primary_actions", value: 1, unit: "acci\xF3n por escena", adaptation: "Divide tareas m\xE1s largas en escenas; evita automatismos que el producto no tiene." }, { key: "state_transition", value: [250, 600], unit: "ms de partida", adaptation: "Ajusta a legibilidad y al ritmo de la explicaci\xF3n; no representa latencia real." }, { key: "result_hold", value: [1, 3], unit: "s de partida", adaptation: "Aumenta tiempo o reduce contenido cuando el estado final requiera lectura." }],
|
|
541
|
+
["Flujo de interacci\xF3n definido y contenido exacto revisado.", "Componentes y motor de composici\xF3n disponibles; uso autorizado de una marca si la UI la representa."],
|
|
542
|
+
["La simulaci\xF3n se puede confundir con prueba real de funcionamiento o rendimiento.", "La simplificaci\xF3n altera el significado de una acci\xF3n importante."]
|
|
543
|
+
),
|
|
544
|
+
composition(
|
|
545
|
+
[
|
|
546
|
+
"editorial-atlas",
|
|
547
|
+
"\xCDndice editorial",
|
|
548
|
+
"Una colecci\xF3n organizada se abre para descubrir un detalle.",
|
|
549
|
+
"Presentar una colecci\xF3n y mantener su \xEDndice visible mientras se destaca un elemento.",
|
|
550
|
+
["Define los elementos reales de la colecci\xF3n y una clasificaci\xF3n que ayude al relato.", "Comp\xF3n tarjetas en una ret\xEDcula editable de JavaScript con n\xFAmeros y t\xEDtulos separados de sus im\xE1genes.", "Revela grupos conservando posiciones; abre una tarjeta protagonista mientras el \xEDndice mantiene la orientaci\xF3n.", "Sost\xE9n el detalle y regresa al conjunto solo si ayuda a comparar; no repitas im\xE1genes para aparentar una colecci\xF3n mayor."],
|
|
551
|
+
[{ key: "visible_cards", value: [3, 9], unit: "tarjetas por estado", adaptation: "Limita el grupo a recursos distintos disponibles; pagina o agrupa si las etiquetas no se leen." }, { key: "active_card", value: 1, unit: "foco principal", adaptation: "Mant\xE9n las otras tarjetas como contexto, sin animar todos los focos a la vez." }, { key: "detail_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a voz y lectura; no representa un tiempo medido del referente." }],
|
|
552
|
+
["Colecci\xF3n de im\xE1genes o clips autorizados, t\xEDtulos confirmados y criterio de agrupaci\xF3n.", "Motor de composici\xF3n disponible para ret\xEDcula, m\xE1scaras y texto editable."],
|
|
553
|
+
["La ret\xEDcula vuelve ilegible la informaci\xF3n esencial.", "La colecci\xF3n o sus categor\xEDas sugieren variedad, ranking o resultados no acreditados."]
|
|
554
|
+
],
|
|
555
|
+
{ title: "Referencia de composici\xF3n: orate.x", url: "https://www.instagram.com/p/DaQCU16scia/" },
|
|
556
|
+
["1:1: organiza una ret\xEDcula equilibrada y reserva una franja estable para el \xEDndice.", "9:16: usa una o dos columnas, agrupa por estados y ampl\xEDa el foco sin miniaturizar toda la colecci\xF3n.", "16:9: separa \xEDndice y detalle en zonas laterales; redistribuye tarjetas sin estirar im\xE1genes."]
|
|
557
|
+
),
|
|
558
|
+
composition(
|
|
559
|
+
[
|
|
560
|
+
"object-headline",
|
|
561
|
+
"Objeto protagonista",
|
|
562
|
+
"Un objeto atraviesa un titular y organiza una portada editorial.",
|
|
563
|
+
"Dar protagonismo a un objeto manteniendo su identidad y una jerarqu\xEDa tipogr\xE1fica clara.",
|
|
564
|
+
["Parte de una imagen fiel del objeto o identifica claramente un objeto ficticio ilustrativo.", "Separa el sujeto con alfa limpio y comp\xF3n el titular detr\xE1s, conservando las letras necesarias para comprenderlo.", "A\xF1ade pocas fichas editables en JavaScript; cambia el \xE9nfasis entre titular, silueta y detalle sin inventar un despiece o giro 3D.", "Comprueba bordes, sombra y estado de lectura; si no hay recorte fiable, utiliza la imagen \xEDntegra dentro de un marco deliberado."],
|
|
565
|
+
[{ key: "subject_height", value: [45, 75], unit: "% del encuadre", adaptation: "Respeta proporciones, detalles y \xE1rea segura del producto; no lo estires para llenar la portada." }, { key: "headline_lines", value: [1, 3], unit: "l\xEDneas", adaptation: "Recomp\xF3n con la fuente de marca y evita oclusiones que cambien el significado." }, { key: "support_labels", value: [0, 3], unit: "fichas", adaptation: "Incluye \xFAnicamente datos confirmados; omite decoraci\xF3n que parezca una especificaci\xF3n." }],
|
|
566
|
+
["Imagen autorizada del objeto y recorte limpio o alternativa de encuadre completo.", "Titular confirmado, fuente disponible y capas editables."],
|
|
567
|
+
["La oclusi\xF3n oculta caracter\xEDsticas relevantes o impide leer el titular.", "La puesta en escena sugiere propiedades, fabricante o resultados no confirmados."]
|
|
568
|
+
],
|
|
569
|
+
{ title: "Referencia de composici\xF3n: francogalvanads", url: "https://www.instagram.com/p/Ddb0qB1ArrV/" },
|
|
570
|
+
["1:1: centra la relaci\xF3n entre silueta y titular; coloca fichas en m\xE1rgenes despejados.", "9:16: reparte el titular en altura y lleva las fichas debajo del objeto, conservando la silueta completa.", "16:9: ampl\xEDa la jerarqu\xEDa horizontal con columnas laterales; no agrandes el objeto a costa de cortar sus detalles."]
|
|
571
|
+
),
|
|
572
|
+
composition(
|
|
573
|
+
[
|
|
574
|
+
"visual-correspondence",
|
|
575
|
+
"De idea a imagen",
|
|
576
|
+
"Dos representaciones conservan su lugar para poder compararse.",
|
|
577
|
+
"Explicar una relaci\xF3n entre un sujeto y su interpretaci\xF3n sin perder la referencia original.",
|
|
578
|
+
["Selecciona dos representaciones del mismo sujeto y declara qu\xE9 relaci\xF3n est\xE1 realmente sustentada.", "Mant\xE9n visible la fuente mientras introduces la interpretaci\xF3n en un panel separado y de escala comparable.", "Comp\xF3n marcos, un detalle activo y anotaciones exactas en JavaScript; las conexiones describen lo visible, no una equivalencia t\xE9cnica imaginada.", "Sost\xE9n ambos paneles para comparar; distingue estudio visual, propuesta y documentaci\xF3n real."],
|
|
579
|
+
[{ key: "representations", value: 2, unit: "paneles", adaptation: "No sustituir la fuente durante la comparaci\xF3n; separa un tercer argumento en otro estado." }, { key: "active_detail", value: 1, unit: "correspondencia por estado", adaptation: "Apunta a un detalle comprobable en ambos recursos, sin convertir similitud en causalidad." }, { key: "comparison_hold", value: [2, 4], unit: "s de partida", adaptation: "Extiende la lectura o reduce anotaciones seg\xFAn voz y resoluci\xF3n." }],
|
|
580
|
+
["Dos recursos autorizados con correspondencia revisada y escala comprensible.", "Anotaciones verificables en capas editables; motor disponible para la comparaci\xF3n."],
|
|
581
|
+
["La interpretaci\xF3n se presenta como plano, diagn\xF3stico o mejora t\xE9cnicamente demostrada sin evidencia.", "Los recursos no corresponden al mismo sujeto y el montaje oculta esa diferencia."]
|
|
582
|
+
],
|
|
583
|
+
{ title: "Referencia de composici\xF3n: valen.bece", url: "https://www.instagram.com/p/DcuJnb6JUUK/" },
|
|
584
|
+
["1:1: usa paneles equilibrados y una franja de anotaciones que no tape la correspondencia.", "9:16: apila origen e interpretaci\xF3n manteniendo ambos visibles; acorta las notas antes de reducir el sujeto.", "16:9: coloca los paneles lado a lado y reserva el centro o margen inferior para una relaci\xF3n por estado."]
|
|
585
|
+
),
|
|
586
|
+
composition(
|
|
587
|
+
[
|
|
588
|
+
"context-triptych",
|
|
589
|
+
"Tres perspectivas",
|
|
590
|
+
"Tres ventanas conectan un tema desde contextos complementarios.",
|
|
591
|
+
"Mostrar perspectivas relacionadas de forma simult\xE1nea, con un foco que gu\xEDa la lectura.",
|
|
592
|
+
["Elige tres contextos complementarios y confirma qu\xE9 v\xEDnculo puede afirmar el guion.", "Comp\xF3n tres ventanas con m\xE1scaras y r\xF3tulos independientes; respeta el formato de apoyo elegido y los recursos autorizados.", "Si hay clips, revisa sus acciones y da protagonismo a una ventana sin deformar las otras; las entradas y cambios de foco son decisiones nuevas de edici\xF3n.", "Deja leer el conjunto sin afirmar continuidad de producci\xF3n, sincron\xEDa o causalidad que las fuentes no demuestren."],
|
|
593
|
+
[{ key: "windows", value: 3, unit: "perspectivas", adaptation: "Cada ventana debe aportar informaci\xF3n distinta; evita duplicar un clip para llenar la composici\xF3n." }, { key: "active_window", value: 1, unit: "foco de lectura", adaptation: "Los otros paneles pueden permanecer en reposo; no es necesario reproducir tres acciones intensas simult\xE1neamente." }, { key: "focus_hold", value: [1, 3], unit: "s de partida", adaptation: "Ancla el foco a voz o acci\xF3n revisada y preserva tiempo para comprender el conjunto." }],
|
|
594
|
+
["Tres recursos autorizados con relaci\xF3n editorial expl\xEDcita; clips reales disponibles cuando se requiera footage.", "Motor disponible para m\xE1scaras, recortes y control independiente de reproducci\xF3n."],
|
|
595
|
+
["La simultaneidad inventa una relaci\xF3n causal o un proceso real no documentado.", "Los tres paneles ocultan acciones, rostros o detalles necesarios por falta de espacio."]
|
|
596
|
+
],
|
|
597
|
+
{ title: "Referencia de composici\xF3n: metromedia.house", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
|
|
598
|
+
["1:1: distribuye tres ventanas con un foco mayor y dos apoyos; conserva lectura en cada recorte.", "9:16: apila tres franjas o un foco y dos apoyos; ajusta cada encuadre al sujeto sin estirar los clips.", "16:9: usa tres columnas legibles; ampliar la activa redistribuye las m\xE1scaras y conserva las proporciones del material."]
|
|
599
|
+
),
|
|
600
|
+
composition(
|
|
601
|
+
[
|
|
602
|
+
"process-filmstrip",
|
|
603
|
+
"Historia de un proceso",
|
|
604
|
+
"Una tarjeta de la secuencia se convierte en la escena principal.",
|
|
605
|
+
"Recorrer fases verificadas de una historia conservando una referencia de progreso.",
|
|
606
|
+
["Define las fases y su orden; utiliza nombres de etapa cuando no existan fechas confirmadas.", "Comp\xF3n un riel de tarjetas y una marca de fase en JavaScript, separados del material visual.", "Desplaza el foco hacia una tarjeta y ampl\xEDa su marco hasta la escena; conserva una ancla de progreso para orientar al espectador.", "Sost\xE9n la etapa y contin\xFAa solo cuando el relato lo justifique; identifica los resultados conceptuales y no los presentes como transformaci\xF3n documentada."],
|
|
607
|
+
[{ key: "stages", value: [3, 5], unit: "etapas por secuencia", adaptation: "El orden procede del guion y la evidencia, no de una cronolog\xEDa inventada." }, { key: "active_frame", value: 1, unit: "tarjeta protagonista", adaptation: "La expansi\xF3n cambia el encuadre, no las proporciones ni el contenido del recurso." }, { key: "stage_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a la explicaci\xF3n y deja leer el nombre de cada fase." }],
|
|
608
|
+
["Recursos autorizados por etapa, orden confirmado y distinci\xF3n entre estado real y propuesta.", "Motor de composici\xF3n disponible para riel, m\xE1scara y r\xF3tulos editables."],
|
|
609
|
+
["La secuencia inventa fechas o presenta im\xE1genes sint\xE9ticas como evidencia de un resultado real.", "El encuadre expandido pierde el detalle que explica la etapa."]
|
|
610
|
+
],
|
|
611
|
+
{ title: "Referencia de composici\xF3n: metromedia.house", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
|
|
612
|
+
["1:1: sit\xFAa el riel junto al borde inferior y reserva el centro para expandir la tarjeta activa.", "9:16: reorganiza el riel en columna o una franja breve; muestra las etapas pr\xF3ximas sin miniaturizar toda la historia.", "16:9: recorre las tarjetas horizontalmente y ampl\xEDa la activa dentro del \xE1rea segura, manteniendo un indicador de fase."]
|
|
613
|
+
),
|
|
614
|
+
composition(
|
|
615
|
+
[
|
|
616
|
+
"tactile-notes",
|
|
617
|
+
"Mesa de ideas",
|
|
618
|
+
"Una superficie estable re\xFAne notas y detalles por estados.",
|
|
619
|
+
"Construir una idea mediante un espacio de trabajo tangible con continuidad visual.",
|
|
620
|
+
["Define una superficie estable y los elementos que realmente forman parte de la explicaci\xF3n.", "Prepara el fondo y las capas independientes; si solo hay una foto plana, limita la animaci\xF3n a nuevas notas y trazos de c\xF3digo.", "Introduce fichas, anotaciones y marcas por estados con posiciones y escala consistentes, sin simular movimiento independiente de objetos incrustados.", "Sost\xE9n la idea completa; describe el tratamiento como composici\xF3n por estados salvo que una captura stop motion est\xE9 corroborada."],
|
|
621
|
+
[{ key: "note_layers", value: [2, 5], unit: "capas editables", adaptation: "Reduce las notas si compiten con el sujeto o requieren letra peque\xF1a." }, { key: "surface", value: "Encuadre y referencias espaciales estables", adaptation: "Conserva perspectiva, sombras y escala coherentes; no impongas madera o papel si contradicen el contenido." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Decide la cadencia seg\xFAn lectura y voz; no es una medici\xF3n del referente ni prueba de captura cuadro a cuadro." }],
|
|
622
|
+
["Superficie y recursos propios o autorizados; capas separadas para cualquier elemento que cambie de posici\xF3n.", "Texto confirmado, fuente disponible y motor de composici\xF3n para notas y estados."],
|
|
623
|
+
["La animaci\xF3n fingir\xEDa que un objeto de una imagen plana puede moverse por separado.", "Las texturas, notas o capas ocultan informaci\xF3n esencial o simulan documentos aut\xE9nticos."]
|
|
624
|
+
],
|
|
625
|
+
{ title: "Referencia de composici\xF3n: anatta.ha", url: "https://www.instagram.com/p/DYV-3gEty9u/" },
|
|
626
|
+
["1:1: agrupa notas alrededor de un foco estable y deja reposar el estado completo.", "9:16: recorre la idea de arriba abajo con pocas notas por estado; recomp\xF3n la mesa sin encoger toda la escena.", "16:9: organiza zonas de material y anotaciones con una secuencia clara; conserva escala y perspectiva al redistribuir capas."]
|
|
627
|
+
),
|
|
628
|
+
composition(
|
|
629
|
+
[
|
|
630
|
+
"subject-depth",
|
|
631
|
+
"Sujeto en primer plano",
|
|
632
|
+
"Una persona conserva protagonismo mientras el t\xEDtulo ocupa un plano detr\xE1s.",
|
|
633
|
+
"Dar jerarqu\xEDa humana al mensaje mediante profundidad y oclusi\xF3n controladas.",
|
|
634
|
+
["Parte de un retrato o clip autorizado y un titular breve que corresponda al mensaje.", "Separa sujeto, t\xEDtulo y fondo; al usar v\xEDdeo, revisa la m\xE1scara durante toda la acci\xF3n, incluidos pelo, manos y desenfoque.", "Ubica el t\xEDtulo detr\xE1s del sujeto con suficiente texto libre para reconocerlo; anima las capas editables sin reconstruir rasgos ni inventar movimiento del cuerpo.", "Sost\xE9n una lectura clara y reduce la oclusi\xF3n o usa un panel adyacente si el recorte pierde calidad."],
|
|
635
|
+
[{ key: "depth_planes", value: 3, unit: "planos", adaptation: "Fondo, t\xEDtulo y sujeto; no a\xF1ade perspectiva o geometr\xEDa real que no existe en la fuente." }, { key: "headline_words", value: [2, 6], unit: "palabras", adaptation: "Adapta tama\xF1o y saltos a la marca y evita tapar las letras que distinguen el mensaje." }, { key: "subject_share", value: [40, 70], unit: "% de altura", adaptation: "Conserva rostro, gestos y zonas seguras; la separaci\xF3n depende del recurso real." }],
|
|
636
|
+
["Retrato propio o autorizado y m\xE1scara fiable por frame si hay movimiento.", "Motor disponible para capas, m\xE1scara y texto editable con las fuentes de marca."],
|
|
637
|
+
["La oclusi\xF3n impide leer el t\xEDtulo o una m\xE1scara deficiente altera la identidad.", "El recorte elimina una acci\xF3n esencial, atribuye palabras al sujeto o modifica un testimonio."]
|
|
638
|
+
],
|
|
639
|
+
{ title: "Referencia de oclusi\xF3n entre sujeto y titular", url: "https://www.instagram.com/p/DZJQm-MgeFL/" },
|
|
640
|
+
["1:1: centra el retrato y reparte el titular en dos l\xEDneas detr\xE1s de los hombros; conserva letras de anclaje visibles.", "9:16: apila titular y figura en profundidad, con rostro libre y m\xE1rgenes para subt\xEDtulos; no ampl\xEDes el cuerpo para llenar vac\xEDos.", "16:9: desplaza el sujeto a un tercio y prolonga el t\xEDtulo detr\xE1s con un bloque legible lateral; recomp\xF3n la m\xE1scara sin estirar el retrato."]
|
|
641
|
+
),
|
|
642
|
+
composition(
|
|
643
|
+
[
|
|
644
|
+
"route-journal",
|
|
645
|
+
"Diario de ruta",
|
|
646
|
+
"Un lugar y un recorrido esquem\xE1tico se conectan con notas breves.",
|
|
647
|
+
"Orientar una historia espacial sin confundir un mapa editorial con navegaci\xF3n precisa.",
|
|
648
|
+
["Confirma lugares, orden y relaci\xF3n del recorrido; distingue coordenadas verificadas de una ruta conceptual.", "Combina material autorizado del lugar con un trazado editable y puntos de parada; la referencia apoya el contexto de lugar, no demuestra un mapa animado.", "Revela el recorrido por etapas con un marcador y una nota activa, sin inventar calles, distancias, tiempos ni geolocalizaci\xF3n.", "Mant\xE9n el lugar visible al mostrar el esquema; rotula como ilustrativo cualquier trazado no geogr\xE1fico y revisa permisos de mapas utilizados."],
|
|
649
|
+
[{ key: "stops", value: [2, 4], unit: "paradas", adaptation: "El orden viene del relato; menos puntos favorecen orientaci\xF3n y lectura en m\xF3vil." }, { key: "active_note", value: 1, unit: "nota por estado", adaptation: "Solo incluye nombres y datos confirmados; no deduce ubicaci\xF3n a partir de una imagen sint\xE9tica." }, { key: "route_hold", value: [1, 3], unit: "s de partida", adaptation: "La duraci\xF3n permite leer, no representa tiempo de viaje ni velocidad real." }],
|
|
650
|
+
["Lugares y recorrido confirmados, o identificaci\xF3n expl\xEDcita del esquema como ilustrativo.", "Material de lugar autorizado y motor disponible para trazo, marcador y etiquetas; mapa licenciado si se requiere precisi\xF3n."],
|
|
651
|
+
["La escena necesita navegaci\xF3n o distancias exactas y no hay datos fiables.", "Revela ubicaciones privadas o presenta un entorno generado como registro de un viaje real."]
|
|
652
|
+
],
|
|
653
|
+
{ title: "Apoyo visual de lugar; la ruta es una adaptaci\xF3n original", url: "https://www.instagram.com/p/Dasq8-6tbt9/" },
|
|
654
|
+
["1:1: reserva un panel al lugar y otro al esquema con una nota activa; evita un mapa lleno de etiquetas.", "9:16: coloca el lugar arriba y el recorrido esquem\xE1tico debajo, conectados por la misma etiqueta; redistribuye los nodos verticalmente sin alterar una geograf\xEDa real.", "16:9: combina una ventana amplia del lugar con el esquema lateral y notas cortas; conserva la escala de los mapas reales y usa r\xF3tulos externos."]
|
|
655
|
+
),
|
|
656
|
+
composition(
|
|
657
|
+
[
|
|
658
|
+
"focus-reveal",
|
|
659
|
+
"Del detalle al conjunto",
|
|
660
|
+
"Un detalle abre el encuadre hasta mostrar su contexto.",
|
|
661
|
+
"Conectar una textura o funci\xF3n con el objeto al que realmente pertenece.",
|
|
662
|
+
["Elige un recurso autorizado de suficiente resoluci\xF3n que contenga tanto el detalle como el objeto completo.", "Define un recorte inicial y un encuadre final del mismo recurso; los planos macro y contextuales de la referencia apoyan la escala, no corroboran un revelado continuo del mismo objeto.", "Ampl\xEDa la ventana de encuadre o reduce el acercamiento sin generar partes ocultas; si son tomas distintas, usa un corte expl\xEDcito y verifica identidad y continuidad.", "Sost\xE9n el conjunto con una etiqueta editable que explique el detalle sin a\xF1adir propiedades no demostradas."],
|
|
663
|
+
[{ key: "start_crop", value: [25, 45], unit: "% del ancho de la fuente", adaptation: "Limita el acercamiento por resoluci\xF3n real y por la informaci\xF3n que se debe reconocer." }, { key: "reveal_duration", value: [800, 1600], unit: "ms de partida", adaptation: "Ajusta a narraci\xF3n y movimiento ya presentes; con movimiento reducido muestra detalle y conjunto en estados." }, { key: "end_context", value: "Objeto reconocible y margen contextual", adaptation: "No exige revelar una cara posterior o entorno inexistente en el material." }],
|
|
664
|
+
["Mismo objeto y detalle verificados, con resoluci\xF3n suficiente o clip continuo revisado.", "Motor disponible para recorte y escala; cualquier reconstrucci\xF3n exige autorizaci\xF3n y identificaci\xF3n aparte."],
|
|
665
|
+
["El detalle y el conjunto pertenecen a objetos distintos o la ampliaci\xF3n inventa partes.", "El reencuadre pierde nitidez, contradice el material real o oculta una advertencia del producto."]
|
|
666
|
+
],
|
|
667
|
+
{ title: "Apoyo de escala macro y contextual; revelado continuo propuesto", url: "https://www.instagram.com/p/DbZDnSpyilk/" },
|
|
668
|
+
["1:1: abre desde un detalle desplazado al centro hacia el objeto completo, conservando un \xE1rea breve para la etiqueta.", "9:16: usa un recorrido de encuadre vertical solo si la fuente lo permite; termina con objeto y explicaci\xF3n apilados sin recortar su funci\xF3n.", "16:9: expande el recorte hacia un plano contextual y sit\xFAa la etiqueta en el espacio libre lateral; no estires una foto vertical."]
|
|
669
|
+
),
|
|
670
|
+
composition(
|
|
671
|
+
[
|
|
672
|
+
"transformation-wipe",
|
|
673
|
+
"Cambio en paralelo",
|
|
674
|
+
"Dos estados alineados comparten un encuadre y un barrido de comparaci\xF3n.",
|
|
675
|
+
"Hacer visible una diferencia verificable manteniendo constantes las referencias espaciales.",
|
|
676
|
+
["Confirma qu\xE9 comparan los dos estados y alinea encuadre, escala y puntos reconocibles.", "Usa etiquetas antes/despu\xE9s solo si existe evidencia temporal; para variantes o demos utiliza propuesta A/B.", "Mueve una m\xE1scara sobre im\xE1genes alineadas o separa paneles si la alineaci\xF3n no es fiable; el barrido es una adaptaci\xF3n nueva de una comparaci\xF3n observada, no un efecto medido.", "Sost\xE9n ambos estados con etiquetas legibles y conserva diferencias aut\xE9nticas sin retocar selectivamente para exagerar un resultado."],
|
|
677
|
+
[{ key: "states", value: 2, unit: "estados comparables", adaptation: "La relaci\xF3n debe estar documentada o identificada como variante conceptual." }, { key: "wipe_range", value: [10, 90], unit: "% del encuadre", adaptation: "Deja contexto para identificar ambos estados; usa corte o paneles est\xE1ticos con movimiento reducido." }, { key: "comparison_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta al detalle relevante y no deduzcas eficacia, duraci\xF3n del proceso ni causalidad." }],
|
|
678
|
+
["Dos recursos autorizados comparables con etiquetas y relaci\xF3n confirmadas.", "Motor disponible para m\xE1scaras y alineaci\xF3n; m\xE9todo de paneles cuando cambien punto de vista o geometr\xEDa."],
|
|
679
|
+
["La comparaci\xF3n mezcla sujetos, \xE1ngulos o condiciones que enga\xF1an sobre el cambio.", "Los recursos sint\xE9ticos se presentan como prueba de mejora, tratamiento o fabricaci\xF3n real."]
|
|
680
|
+
],
|
|
681
|
+
{ title: "Referencia de comparaci\xF3n alineada; barrido propuesto", url: "https://www.instagram.com/p/Dcggcv6xgAn/" },
|
|
682
|
+
["1:1: alinea ambos estados en un marco com\xFAn y ubica etiquetas en bordes opuestos sin tapar la diferencia.", "9:16: usa estados apilados o un barrido vertical cuando conserve puntos de referencia; elige paneles si el recorte oculta contexto.", "16:9: compara en un lienzo compartido con barrido horizontal o dos paneles equivalentes; no deformes una versi\xF3n para hacerla coincidir."]
|
|
683
|
+
),
|
|
684
|
+
composition(
|
|
685
|
+
[
|
|
686
|
+
"evidence-spotlight",
|
|
687
|
+
"Detalle con evidencia",
|
|
688
|
+
"Un recorte destacado conecta una fuente con una anotaci\xF3n verificable.",
|
|
689
|
+
"Dirigir la atenci\xF3n hacia el detalle que respalda una afirmaci\xF3n concreta.",
|
|
690
|
+
["Selecciona la fuente autorizada y confirma qu\xE9 pasaje o detalle sostiene la afirmaci\xF3n; registra procedencia sin exponer informaci\xF3n privada.", "Mant\xE9n una vista contextual y un recorte ampliado del mismo recurso, unidos por una llamada editable.", "Marca una regi\xF3n con flecha, subrayado o contorno sin alterar su contenido; los datos y etiquetas se componen en c\xF3digo y se verifican contra la fuente.", "Deja tiempo para leer y distingue evidencia documental de una muestra ficticia; la anotaci\xF3n no convierte una imagen generada en prueba."],
|
|
691
|
+
[{ key: "callouts", value: [1, 2], unit: "llamadas por estado", adaptation: "Cada llamada debe se\xF1alar un dato concreto; divide pasajes largos en estados sucesivos." }, { key: "detail_share", value: [35, 60], unit: "% del panel", adaptation: "La ampliaci\xF3n depende de resoluci\xF3n y legibilidad real, no de OCR supuesto." }, { key: "reading_hold", value: [2, 4], unit: "s de partida", adaptation: "Ampl\xEDa el tiempo para texto denso y mant\xE9n contexto suficiente para no tergiversar la fuente." }],
|
|
692
|
+
["Fuente autorizada, procedencia y afirmaci\xF3n verificadas; datos privados eliminados antes de composici\xF3n.", "Motor disponible para recortes, llamadas y texto editable con contraste suficiente."],
|
|
693
|
+
["La fuente no respalda la afirmaci\xF3n o el recorte cambia su sentido.", "La demo, el OCR no revisado o una imagen sint\xE9tica se presentar\xEDan como evidencia real."]
|
|
694
|
+
],
|
|
695
|
+
{ title: "Referencia de pasaje documental con flecha y contorno", url: "https://www.instagram.com/p/DZXtFEbKxdb/" },
|
|
696
|
+
["1:1: conserva la fuente en un panel y sit\xFAa la ampliaci\xF3n junto a una sola llamada; prioriza la lectura del detalle.", "9:16: apila contexto, detalle y conclusi\xF3n breve; evita convertir una p\xE1gina completa en texto diminuto.", "16:9: coloca fuente y ampliaci\xF3n lado a lado con una conexi\xF3n corta; preserva pie de fuente y escala legible."]
|
|
697
|
+
),
|
|
698
|
+
composition(
|
|
699
|
+
[
|
|
700
|
+
"interface-walkthrough",
|
|
701
|
+
"Recorrido de interfaz",
|
|
702
|
+
"Una interfaz recorre una acci\xF3n con cursor, foco y confirmaci\xF3n.",
|
|
703
|
+
"Explicar una tarea digital mediante estados claros y un resultado comprensible.",
|
|
704
|
+
["Define una sola tarea y estados reales verificados o una interfaz ficticia claramente identificada; nunca presenta la demo como integraci\xF3n disponible.", "Comp\xF3n paneles, textos y cursor en capas editables; elimina datos privados si el recurso es una captura real.", "Gu\xEDa hacia un control, ampl\xEDa la regi\xF3n \xFAtil y muestra el siguiente estado; el cursor y la magnificaci\xF3n son adaptaci\xF3n propuesta, no l\xF3gica de aplicaci\xF3n demostrada por los frames.", "Cierra con una confirmaci\xF3n solo cuando corresponda al comportamiento verificado o a la demo rotulada; comprueba texto, destino y l\xEDmites de la acci\xF3n."],
|
|
705
|
+
[{ key: "steps", value: [2, 4], unit: "estados por tarea", adaptation: "Divide flujos largos en escenas; no simules automatizaci\xF3n no disponible." }, { key: "focus_regions", value: 1, unit: "regi\xF3n activa", adaptation: "El resto conserva contexto estable; el cursor no debe competir con la informaci\xF3n que se explica." }, { key: "step_hold", value: [1, 3], unit: "s de partida", adaptation: "Ajusta a lectura y explicaci\xF3n; estados est\xE1ticos y foco por corte funcionan con movimiento reducido." }],
|
|
706
|
+
["Flujo real revisado o demo ficticia rotulada, sin credenciales ni datos de clientes.", "Motor disponible para estados de interfaz, cursor y magnificaci\xF3n; acceso y permisos confirmados por separado."],
|
|
707
|
+
["La secuencia inventa capacidades de un producto o promete una integraci\xF3n habilitada.", "Una ampliaci\xF3n vuelve ilegible el control, oculta un error real o filtra datos privados."]
|
|
708
|
+
],
|
|
709
|
+
{ title: "Referencia de estados de editor, men\xFA y confirmaci\xF3n", url: "https://www.instagram.com/p/DdocHKLq7i0/" },
|
|
710
|
+
["1:1: muestra un panel de tarea y ampl\xEDa la regi\xF3n activa dentro del mismo marco sin perder el estado anterior.", "9:16: recomp\xF3n la interfaz como paneles legibles o usa un recorte guiado con contexto; no reduzcas un escritorio completo a m\xF3vil.", "16:9: conserva la interfaz amplia y usa una lente o panel lateral de detalle; el cursor mantiene una ruta corta entre controles."]
|
|
711
|
+
),
|
|
712
|
+
composition(
|
|
713
|
+
[
|
|
714
|
+
"quote-portrait",
|
|
715
|
+
"Retrato y frase",
|
|
716
|
+
"Un retrato acompa\xF1a una frase breve con jerarqu\xEDa editorial.",
|
|
717
|
+
"Relacionar una idea con una presencia humana sin inventar su autor\xEDa.",
|
|
718
|
+
["Confirma derechos del retrato, texto exacto y atribuci\xF3n; si son recursos sint\xE9ticos o texto de muestra, identif\xEDcalos como ilustrativos.", "Comp\xF3n retrato y frase como elementos independientes; el referente aporta persona y palabras de anclaje, no una cita atribuida equivalente.", "Revela la frase en bloques legibles y conserva rostro y gesto libres; no generes movimiento labial ni voz para atribuirle palabras al sujeto.", "A\xF1ade nombre o fuente solo cuando est\xE9 verificado; sin atribuci\xF3n usa una idea editorial identificada, evitando comillas que simulen un testimonio."],
|
|
719
|
+
[{ key: "quote_words", value: [6, 18], unit: "palabras orientativas", adaptation: "Respeta el texto exacto autorizado; divide una cita larga sin cambiar su significado." }, { key: "text_blocks", value: [1, 3], unit: "bloques", adaptation: "La jerarqu\xEDa usa las fuentes de marca y no exige cambiar de familia o inventar una firma." }, { key: "reading_hold", value: [2, 5], unit: "s de partida", adaptation: "Ajusta al n\xFAmero de palabras y al p\xFAblico; permite un estado completo legible." }],
|
|
720
|
+
["Retrato autorizado y texto con atribuci\xF3n confirmada o identificaci\xF3n expl\xEDcita como idea editorial de muestra.", "Motor disponible para tipograf\xEDa editable y composici\xF3n; fuentes de marca instaladas."],
|
|
721
|
+
["La composici\xF3n har\xEDa parecer que una persona real o sint\xE9tica pronunci\xF3 una frase no verificada.", "El texto cambia el sentido del testimonio, oculta identidad relevante o necesita una voz no autorizada."]
|
|
722
|
+
],
|
|
723
|
+
{ title: "Apoyo de retrato y palabras de anclaje; cita editorial propuesta", url: "https://www.instagram.com/p/DdKae1xzRao/" },
|
|
724
|
+
["1:1: equilibra retrato y frase en dos zonas con una atribuci\xF3n secundaria legible cuando exista.", "9:16: coloca retrato arriba y frase debajo; conserva zona segura para subt\xEDtulos sin superponer dos lecturas densas.", "16:9: usa retrato en un tercio y un bloque de cita amplio en el resto; no expanda el texto sobre rasgos esenciales."]
|
|
725
|
+
),
|
|
726
|
+
composition(
|
|
727
|
+
[
|
|
728
|
+
"type-poster",
|
|
729
|
+
"Cartel en movimiento",
|
|
730
|
+
"Palabras grandes construyen un cartel mediante cambios de estado.",
|
|
731
|
+
"Convertir una idea breve en una jerarqu\xEDa tipogr\xE1fica memorable y legible.",
|
|
732
|
+
["Define un mensaje breve y sus palabras clave sin alterar su orden o sentido.", "Comp\xF3n el cartel con fuentes disponibles de marca; escala, peso y espacio crean contraste sin exigir la paleta del referente.", "Anima entradas, m\xE1scaras o desplazamientos entre estados; la captura de texto grande apoya la jerarqu\xEDa, no acredita una curva o cadencia espec\xEDfica.", "Sost\xE9n el mensaje completo y revisa acentos, interlineado y recortes; con movimiento reducido muestra estados estables sin parpadeos."],
|
|
733
|
+
[{ key: "headline_words", value: [2, 8], unit: "palabras por cartel", adaptation: "Divide mensajes extensos en carteles completos, no en fragmentos que cambien la interpretaci\xF3n." }, { key: "type_levels", value: [2, 3], unit: "niveles de jerarqu\xEDa", adaptation: "Usa peso, tama\xF1o y espaciado dentro de las fuentes de marca; no impone una tipograf\xEDa nueva." }, { key: "state_hold", value: [800, 1800], unit: "ms de partida", adaptation: "Ajusta a lectura real y evita destellos o alternancias r\xE1pidas de alto contraste." }],
|
|
734
|
+
["Texto confirmado, fuentes de marca disponibles y motor de composici\xF3n tipogr\xE1fica editable.", "Revisi\xF3n de caracteres espa\xF1oles, contraste y \xE1reas seguras en cada formato."],
|
|
735
|
+
["El texto es demasiado largo para leerse al ritmo propuesto.", "La animaci\xF3n recorta palabras esenciales, convierte decoraci\xF3n en un claim o requiere parpadeos intensos."]
|
|
736
|
+
],
|
|
737
|
+
{ title: "Referencia de palabra tipogr\xE1fica de gran escala", url: "https://www.instagram.com/p/DdKae1xzRao/" },
|
|
738
|
+
["1:1: organiza una palabra dominante y una l\xEDnea de apoyo con m\xE1rgenes consistentes y un estado final completo.", "9:16: apila l\xEDneas cortas de arriba abajo con pausas de lectura; recomp\xF3n los saltos sin cortar palabras ni acentos.", "16:9: extiende la jerarqu\xEDa horizontalmente o contrapone dos bloques; conserva el orden de lectura y evita ampliar una palabra hasta perder sus extremos."]
|
|
739
|
+
)
|
|
740
|
+
]
|
|
741
|
+
};
|
|
742
|
+
function getMediaStyle(id, version) {
|
|
743
|
+
return MEDIA_STYLE_CATALOG.styles.find((style) => style.id === id && (version === void 0 || style.version === version));
|
|
744
|
+
}
|
|
745
|
+
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
746
|
+
var only = (value, fields) => Object.keys(value).every((key) => fields.includes(key));
|
|
747
|
+
function visualTasteIssues(value) {
|
|
748
|
+
if (!record(value) || !only(value, ["density", "energy", "contrast"])) return ["visualTaste supports density, energy and contrast only."];
|
|
749
|
+
const options = { density: ["minimal", "layered"], energy: ["calm", "lively"], contrast: ["soft", "strong"] };
|
|
750
|
+
return Object.entries(options).flatMap(([key, allowed]) => value[key] === void 0 || typeof value[key] === "string" && allowed.includes(value[key]) ? [] : [`Invalid visualTaste ${key}. Omit undecided choices.`]);
|
|
751
|
+
}
|
|
752
|
+
function motionSelectionIssues(value) {
|
|
753
|
+
return value === void 0 || value === "adaptive" || value === "guided" ? [] : ["motionSelection must be adaptive or guided. Omit undecided choices."];
|
|
754
|
+
}
|
|
755
|
+
function motionProductionIssues(value) {
|
|
756
|
+
return value === void 0 || value === "auto" || value === "code" || value === "ai" ? [] : ["motionProduction must be auto, code or ai. Omit undecided choices."];
|
|
757
|
+
}
|
|
758
|
+
function resolveMotionSelection(selections = [], motionSelection) {
|
|
759
|
+
return motionSelection ?? (selections.some((selection) => selection.state === "default" && getMediaStyle(selection.styleId, selection.version)?.category === "motion") ? "guided" : "adaptive");
|
|
760
|
+
}
|
|
761
|
+
function mediaStyleSelectionIssues(value, category) {
|
|
762
|
+
if (!Array.isArray(value) || value.length > 24) return ["mediaStyles must be an array of at most 24 selections."];
|
|
763
|
+
const issues = [], ids = /* @__PURE__ */ new Set(), defaults = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const selection of value) {
|
|
765
|
+
if (!record(selection) || !only(selection, ["styleId", "version", "state"]) || typeof selection.styleId !== "string" || typeof selection.version !== "string" || !["favorite", "default", "avoid"].includes(String(selection.state))) {
|
|
766
|
+
issues.push("Media style selections require styleId, version and favorite/default/avoid state.");
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const style = getMediaStyle(selection.styleId, selection.version);
|
|
770
|
+
if (!style) {
|
|
771
|
+
issues.push(`Unknown media style or unsupported version: ${selection.styleId}@${selection.version}.`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (category && style.category !== category) issues.push(`Media style ${style.id} does not belong to ${category}.`);
|
|
775
|
+
if (ids.has(style.id)) issues.push(`Duplicate media style selection: ${style.id}.`);
|
|
776
|
+
ids.add(style.id);
|
|
777
|
+
if (selection.state === "default") {
|
|
778
|
+
if (defaults.has(style.category)) issues.push(`Only one default media style is allowed for ${style.category}.`);
|
|
779
|
+
defaults.add(style.category);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return issues;
|
|
783
|
+
}
|
|
784
|
+
function mediaStyleOverrideIssues(value) {
|
|
785
|
+
if (!record(value) || !only(value, ["image", "motion"])) return ["mediaStyleOverrides supports image and motion categories only."];
|
|
786
|
+
const issues = [];
|
|
787
|
+
for (const category of ["image", "motion"]) {
|
|
788
|
+
const override = value[category];
|
|
789
|
+
if (override === void 0) continue;
|
|
790
|
+
if (!record(override) || !["inherit", "replace", "disabled"].includes(String(override.mode))) {
|
|
791
|
+
issues.push(`Invalid ${category} media style override.`);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (override.mode === "replace") {
|
|
795
|
+
if (!only(override, ["mode", "selections"])) issues.push(`Unknown ${category} media style override fields.`);
|
|
796
|
+
if (!Array.isArray(override.selections) || !override.selections.length) issues.push(`The ${category} replace mode requires selections; use disabled to clear the category.`);
|
|
797
|
+
issues.push(...mediaStyleSelectionIssues(override.selections, category));
|
|
798
|
+
} else if (!only(override, ["mode"])) issues.push(`The ${category} ${override.mode} mode cannot contain selections.`);
|
|
799
|
+
}
|
|
800
|
+
return issues;
|
|
801
|
+
}
|
|
802
|
+
function resolveMediaPreferences(selections = [], overrides = {}, taste = {}, motionSelection, motionProduction, editorialDelivery, captionsEnabled) {
|
|
803
|
+
const issues = [...mediaStyleSelectionIssues(selections), ...mediaStyleOverrideIssues(overrides), ...visualTasteIssues(taste), ...motionSelectionIssues(motionSelection), ...motionProductionIssues(motionProduction), ...editorialDeliveryIssues(editorialDelivery)];
|
|
804
|
+
if (issues.length) throw new Error(issues.join("\n"));
|
|
805
|
+
const category = (kind) => {
|
|
806
|
+
const override = overrides[kind], mode = override?.mode ?? "inherit";
|
|
807
|
+
const selected = mode === "disabled" ? [] : override?.mode === "replace" ? override.selections : selections.filter((selection) => getMediaStyle(selection.styleId, selection.version).category === kind);
|
|
808
|
+
return { mode, source: mode === "inherit" ? selected.length ? "brand" : "unspecified" : "project", selections: selected.map((selection) => ({ ...selection, name: getMediaStyle(selection.styleId, selection.version).name })) };
|
|
809
|
+
};
|
|
810
|
+
const motion2 = category("motion");
|
|
811
|
+
const effectiveMotionSelection = resolveMotionSelection(motion2.selections, motionSelection);
|
|
812
|
+
const motionSelectionSource = motionSelection !== void 0 ? "brand" : effectiveMotionSelection === "guided" && motion2.source !== "unspecified" ? motion2.source : "implicit";
|
|
813
|
+
if (effectiveMotionSelection === "adaptive") motion2.selections = motion2.selections.map((selection) => selection.state === "default" ? { ...selection, state: "favorite" } : selection);
|
|
814
|
+
const delivery = resolveEditorialDelivery(editorialDelivery, captionsEnabled);
|
|
815
|
+
return { schemaVersion: 1, catalog: { id: MEDIA_STYLE_CATALOG.id, version: MEDIA_STYLE_CATALOG.version }, image: category("image"), motion: motion2, motionSelection: effectiveMotionSelection, motionSelectionSource, motionProduction: motionProduction ?? "auto", motionProductionSource: motionProduction === void 0 ? "implicit" : "brand", ...Object.keys(delivery).length ? { editorialDelivery: delivery } : {}, taste: { ...taste }, policy: "Current explicit instructions govern. Project overrides define effective choices; disabled categories stay disabled. Required brand tokens govern, never preview palettes/fonts. Favorites are candidates; effective avoid entries always exclude. Adaptive motion chooses per scene or none; saved defaults become candidate favorites. Guided motion starts from saved choices and checks fit. Production auto chooses by scene and verified tools; code prefers complete editable JavaScript scenes, ai prefers generated clips. Keep exact text/data in controlled composition. Production preferences prove neither personal integration access nor model availability, rights or permission to spend. Check the current member\u2019s integrations and session tools; files access alone cannot generate. Taste guides intensity, not tokens or a forced recipe. Soft contrast must retain readable text and visible essential information." + (Object.keys(delivery).length ? " Editorial delivery contains explicit brand preferences only; omitted fields are for the agent to decide. Existing avoid and source restrictions constrain delivery; no-SFX overrides a general preference for effects. Silent music/effects does not silence speech, and no narration does not mute ambience. Text density excludes subtitles; B-roll format does not authorize a source. Disabled captions omit their language, treatment and position from the effective view without erasing saved choices; a chosen appearance cannot enable captions. Respect framing and safe areas; karaoke needs verified word timing. Explicit project output settings prevail; otherwise source uses verified speech language and brand uses language.json. CTA goal none excludes every CTA and omits its treatment. Other CTA goals guide each project; confirm final copy, keyword and destination in its brief or direction, never invent them. Spoken CTA grants no new speech or rewriting permission and cannot override no narration. Voiceover authorizes neither cloning nor providers or spending." : "") };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// packages/core/src/editorial-profile.ts
|
|
819
|
+
var profileOptions = {
|
|
820
|
+
experience: ["Estoy empezando", "Ya creo videos", "Edito profesionalmente"],
|
|
821
|
+
role: ["Marca personal / creador", "Negocio / equipo de marketing", "Agencia / editor para clientes"],
|
|
822
|
+
platforms: ["Instagram / Reels", "TikTok", "YouTube Shorts", "YouTube", "LinkedIn"],
|
|
823
|
+
formats: ["A c\xE1mara", "Tutoriales", "Entrevistas", "Vlogs", "Producto / UGC", "Sin aparecer en c\xE1mara"],
|
|
824
|
+
goal: ["Ense\xF1ar", "Generar confianza", "Vender", "Entretener", "Crear comunidad"],
|
|
825
|
+
pace: ["Pausado", "\xC1gil", "Muy din\xE1mico"],
|
|
826
|
+
captions: ["Subt\xEDtulos discretos", "Palabras destacadas", "Sin subt\xEDtulos"],
|
|
827
|
+
sound: ["Voz protagonista", "M\xFAsica discreta", "M\xFAsica y efectos presentes", "Sin m\xFAsica ni efectos"],
|
|
828
|
+
freedom: ["Mantener un estilo consistente", "Explorar seg\xFAn el proyecto"],
|
|
829
|
+
assets: ["Solo mi material", "Se puede proponer material de stock", "Se puede proponer material generado"],
|
|
830
|
+
avoid: ["Sin emojis", "Sin transiciones llamativas", "Sin efectos de sonido", "No reescribir mis palabras", "No usar im\xE1genes generadas"],
|
|
831
|
+
aspects: ["Tipograf\xEDa", "Composici\xF3n", "Colores", "Im\xE1genes", "Ritmo", "Animaci\xF3n", "Sonido"]
|
|
832
|
+
};
|
|
833
|
+
var emptyProfile = () => ({ schemaVersion: 1, experience: "", role: "", niche: "", platforms: [], formats: [], goal: "", pace: "", captions: "", sound: "", freedom: "", assets: "", avoid: [], notes: "", references: [] });
|
|
834
|
+
var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
835
|
+
var string = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max;
|
|
836
|
+
var choices = (value, options) => Array.isArray(value) && value.length <= options.length && value.every((item) => typeof item === "string" && options.includes(item));
|
|
837
|
+
function editorialProfileIssues(value) {
|
|
838
|
+
if (!record2(value)) return ["El perfil editorial debe ser un objeto."];
|
|
839
|
+
const issues = [];
|
|
840
|
+
if (value.schemaVersion !== 1) issues.push("schemaVersion del perfil editorial debe ser 1.");
|
|
841
|
+
const fields = [...Object.keys(emptyProfile()), "mediaStyles", "visualTaste", "motionSelection", "motionProduction", "editorialDelivery"];
|
|
842
|
+
if (Object.keys(value).some((key) => !fields.includes(key))) issues.push("El perfil editorial contiene campos desconocidos.");
|
|
843
|
+
for (const key of ["experience", "role", "goal", "pace", "captions", "sound", "freedom", "assets"]) {
|
|
844
|
+
const selected = value[key];
|
|
845
|
+
if ((key === "experience" || key === "role") && selected === void 0) continue;
|
|
846
|
+
if (typeof selected !== "string" || selected !== "" && !profileOptions[key].includes(selected)) issues.push(`Preferencia editorial inv\xE1lida: ${key}.`);
|
|
847
|
+
}
|
|
848
|
+
for (const key of ["platforms", "formats", "avoid"]) if (!choices(value[key], profileOptions[key])) issues.push(`Opciones editoriales inv\xE1lidas: ${key}.`);
|
|
849
|
+
if (!string(value.niche, 0, 500)) issues.push("niche debe ser texto de hasta 500 caracteres.");
|
|
850
|
+
if (!string(value.notes, 0, 1500)) issues.push("notes debe ser texto de hasta 1500 caracteres.");
|
|
851
|
+
if (!Array.isArray(value.references) || value.references.length > 12) issues.push("references admite hasta 12 referencias.");
|
|
852
|
+
else for (const ref of value.references) {
|
|
853
|
+
if (!record2(ref) || !string(ref.videoId, 1, 150) || !string(ref.exampleId, 1, 150) || typeof ref.timestampMs !== "number" || !Number.isFinite(ref.timestampMs) || ref.timestampMs < 0 || !["like", "avoid"].includes(String(ref.sentiment)) || !choices(ref.aspects, profileOptions.aspects)) issues.push("Referencia editorial inv\xE1lida.");
|
|
854
|
+
}
|
|
855
|
+
if (value.mediaStyles !== void 0) issues.push(...mediaStyleSelectionIssues(value.mediaStyles));
|
|
856
|
+
if (value.visualTaste !== void 0) issues.push(...visualTasteIssues(value.visualTaste));
|
|
857
|
+
issues.push(...motionSelectionIssues(value.motionSelection));
|
|
858
|
+
issues.push(...motionProductionIssues(value.motionProduction));
|
|
859
|
+
issues.push(...editorialDeliveryIssues(value.editorialDelivery));
|
|
860
|
+
return issues;
|
|
861
|
+
}
|
|
862
|
+
function parseEditorialProfile(value) {
|
|
863
|
+
const issues = editorialProfileIssues(value);
|
|
864
|
+
if (issues.length) throw new Error(issues.join("\n"));
|
|
865
|
+
const input = value;
|
|
866
|
+
return { ...input, experience: input.experience ?? "", role: input.role ?? "", platforms: [...input.platforms], formats: [...input.formats], avoid: [...input.avoid], references: input.references.map((ref) => ({ videoId: ref.videoId, exampleId: ref.exampleId, timestampMs: ref.timestampMs, sentiment: ref.sentiment, aspects: [...ref.aspects] })), ...input.mediaStyles !== void 0 ? { mediaStyles: input.mediaStyles.map((selection) => ({ ...selection })) } : {}, ...input.visualTaste !== void 0 ? { visualTaste: { ...input.visualTaste } } : {}, ...input.editorialDelivery !== void 0 ? { editorialDelivery: { ...input.editorialDelivery } } : {} };
|
|
867
|
+
}
|
|
868
|
+
var start = "<!-- editorbrain:editorial-profile:start -->";
|
|
869
|
+
var end = "<!-- editorbrain:editorial-profile:end -->";
|
|
870
|
+
function bounds(content) {
|
|
871
|
+
const a = content.indexOf(start), b = content.indexOf(end);
|
|
872
|
+
if (a === -1 && b === -1) return null;
|
|
873
|
+
if (a === -1 || b < a || content.indexOf(start, a + start.length) !== -1 || content.indexOf(end, b + end.length) !== -1) throw new Error("El perfil fue editado fuera del formulario. Revisa su bloque en Gu\xEDa visual antes de guardar.");
|
|
874
|
+
return [a, b + end.length];
|
|
875
|
+
}
|
|
876
|
+
function readProfile(content) {
|
|
877
|
+
const span = bounds(content);
|
|
878
|
+
if (!span) return emptyProfile();
|
|
879
|
+
const match = content.slice(...span).match(/```json\r?\n([\s\S]*?)\r?\n```/);
|
|
880
|
+
if (!match) throw new Error("No pudimos leer tu perfil. Revisa el bloque en Gu\xEDa visual; conservamos el archivo original.");
|
|
881
|
+
return parseEditorialProfile(JSON.parse(match[1]));
|
|
882
|
+
}
|
|
883
|
+
|
|
203
884
|
// packages/core/src/production-plan.ts
|
|
204
|
-
var
|
|
885
|
+
var record3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
205
886
|
var positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
206
887
|
var nonnegative = (v) => v === 0 || positive(v);
|
|
207
888
|
var hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v);
|
|
208
889
|
var text = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 2e3;
|
|
209
|
-
var range = (v) =>
|
|
890
|
+
var range = (v) => record3(v) && nonnegative(v.startSeconds) && positive(v.endSeconds) && v.endSeconds > v.startSeconds;
|
|
210
891
|
var MAX_ENTRIES = 2e3;
|
|
211
892
|
function validateProductionPlan(value) {
|
|
212
893
|
const issues = [];
|
|
213
|
-
if (!
|
|
894
|
+
if (!record3(value) || value.schemaVersion !== 1 || value.kind !== "editorbrain.production-plan") return { valid: false, issues: ["Expected schemaVersion 1 and kind editorbrain.production-plan."] };
|
|
214
895
|
if (Object.keys(value).some((key) => !["schemaVersion", "kind", "inspections", "uses"].includes(key))) issues.push("Unknown plan fields; keep editorial notes in direction.md and use the documented plan schema.");
|
|
215
896
|
if (!Array.isArray(value.inspections) || value.inspections.length > MAX_ENTRIES) issues.push("inspections must be an array of at most 2000 observations.");
|
|
216
897
|
else value.inspections.forEach((item, index) => {
|
|
217
898
|
const label = `inspections[${index}]`;
|
|
218
|
-
if (!
|
|
899
|
+
if (!record3(item) || !hash(item.sourceSha256) || !["visual", "motion", "audio"].includes(String(item.modality)) || !["sampled_frames", "playback", "listening"].includes(String(item.method))) {
|
|
219
900
|
issues.push(`${label}: invalid source hash, modality or method.`);
|
|
220
901
|
return;
|
|
221
902
|
}
|
|
@@ -233,7 +914,7 @@ function validateProductionPlan(value) {
|
|
|
233
914
|
const ids = /* @__PURE__ */ new Set();
|
|
234
915
|
value.uses.forEach((item, index) => {
|
|
235
916
|
const label = `uses[${index}]`;
|
|
236
|
-
if (!
|
|
917
|
+
if (!record3(item) || typeof item.id !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(item.id) || !hash(item.sourceSha256) || !range(item.source) || !range(item.timeline)) {
|
|
237
918
|
issues.push(`${label}: requires ID, source hash, source and timeline intervals.`);
|
|
238
919
|
return;
|
|
239
920
|
}
|
|
@@ -259,7 +940,7 @@ function checkProductionPlan(inventory, value) {
|
|
|
259
940
|
const findings = checked.issues.map((message) => ({ code: "INVALID_PLAN", severity: "error", message }));
|
|
260
941
|
const sources = /* @__PURE__ */ new Map();
|
|
261
942
|
let inventoryComplete = true;
|
|
262
|
-
if (!
|
|
943
|
+
if (!record3(inventory) || inventory.schemaVersion !== 1 || inventory.kind !== "editorbrain.media-inventory" || inventory.summary === true || typeof inventory.complete !== "boolean" || !Array.isArray(inventory.assets) || inventory.assets.length > 1e4) {
|
|
263
944
|
inventoryComplete = false;
|
|
264
945
|
findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Use the full schemaVersion 1 media inventory saved with --out, not compact stdout. It must declare complete." });
|
|
265
946
|
} else {
|
|
@@ -268,7 +949,7 @@ function checkProductionPlan(inventory, value) {
|
|
|
268
949
|
findings.push({ code: "INVENTORY_INCOMPLETE", severity: "warning", message: "Inventory traversal or probing was incomplete. This plan check covers only recorded assets." });
|
|
269
950
|
}
|
|
270
951
|
for (const asset of inventory.assets) {
|
|
271
|
-
if (!
|
|
952
|
+
if (!record3(asset) || !hash(asset.sha256) || !(asset.durationSeconds === null || nonnegative(asset.durationSeconds)) || asset.streams !== void 0 && (!Array.isArray(asset.streams) || asset.streams.some((s) => !record3(s) || typeof s.type !== "string"))) {
|
|
272
953
|
findings.push({ code: "INVALID_INVENTORY", severity: "error", message: "Inventory assets need a source hash, nonnegative duration or null, and valid optional streams." });
|
|
273
954
|
continue;
|
|
274
955
|
}
|
|
@@ -284,12 +965,12 @@ function checkProductionPlan(inventory, value) {
|
|
|
284
965
|
if (!source) findings.push({ code: "UNKNOWN_SOURCE", severity: "error", message: "A source hash is absent from the selected inventory. Refresh the inventory after changing media.", sourceSha256 });
|
|
285
966
|
return source;
|
|
286
967
|
};
|
|
287
|
-
const inBounds = (
|
|
968
|
+
const inBounds = (end2, source) => source.durationSeconds === null || end2 <= source.durationSeconds + 1e-3;
|
|
288
969
|
for (const inspection of plan.inspections) {
|
|
289
970
|
const source = sourceFor(inspection.sourceSha256);
|
|
290
971
|
if (!source) continue;
|
|
291
972
|
if (source.streams && !source.streams.some((s) => s.type === (inspection.modality === "audio" ? "audio" : "video") && (inspection.modality !== "motion" || !s.attachedPicture))) findings.push({ code: "MODALITY_ABSENT", severity: "error", message: `Declared ${inspection.modality} inspection has no matching stream.`, sourceSha256: source.sha256 });
|
|
292
|
-
if ([...inspection.sampledSeconds ?? [], ...(inspection.intervals ?? []).map((r) => r.endSeconds)].some((
|
|
973
|
+
if ([...inspection.sampledSeconds ?? [], ...(inspection.intervals ?? []).map((r) => r.endSeconds)].some((end2) => !inBounds(end2, source))) findings.push({ code: "INSPECTION_OUT_OF_BOUNDS", severity: "error", message: "Inspection extends past the source duration.", sourceSha256: source.sha256 });
|
|
293
974
|
}
|
|
294
975
|
const validUses = [];
|
|
295
976
|
for (const use of plan.uses) {
|
|
@@ -331,6 +1012,53 @@ function checkProductionPlan(inventory, value) {
|
|
|
331
1012
|
return { schemaVersion: 1, kind: "editorbrain.production-plan-check", valid: !findings.some((f) => f.severity === "error"), inventoryComplete, findings, coverage, repeatedPairs, sourceFilesReverified: false, quality: "not_assessed", observations: "agent_declared" };
|
|
332
1013
|
}
|
|
333
1014
|
|
|
1015
|
+
// packages/core/src/integrations.ts
|
|
1016
|
+
var integrationCatalog = [
|
|
1017
|
+
{ id: "higgsfield", name: "Higgsfield", description: "Im\xE1genes y clips generados para apoyar tu edici\xF3n.", capabilities: ["image", "video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://open.higgsfield.ai/quick-start" },
|
|
1018
|
+
{ id: "elevenlabs", name: "ElevenLabs", description: "Voces, efectos de sonido y m\xFAsica generada.", capabilities: ["voiceover", "sfx", "music"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://elevenlabs.io/docs/overview" },
|
|
1019
|
+
{ id: "suno", name: "Suno", description: "M\xFAsica generada con Suno; el acceso API depende de tu cuenta.", capabilities: ["music"], accessMethods: ["browser", "api", "files"], docsUrl: "https://platform.suno.com/" },
|
|
1020
|
+
{ id: "openai-images", name: "OpenAI Images", description: "Creaci\xF3n y edici\xF3n de im\xE1genes para tu contenido.", capabilities: ["image"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://platform.openai.com/docs/guides/image-generation" },
|
|
1021
|
+
{ id: "pexels", name: "Pexels", description: "Fotos y videos de stock; revisa la licencia y el contexto de uso.", capabilities: ["stock-image", "stock-video"], accessMethods: ["agent", "api", "browser", "files"], docsUrl: "https://www.pexels.com/api/documentation/" },
|
|
1022
|
+
{ id: "assemblyai", name: "AssemblyAI", description: "Transcripci\xF3n con marcas de tiempo para planificar cortes y subt\xEDtulos.", capabilities: ["transcription"], accessMethods: ["agent", "api", "files"], docsUrl: "https://www.assemblyai.com/docs" }
|
|
1023
|
+
];
|
|
1024
|
+
function object2(value) {
|
|
1025
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1026
|
+
}
|
|
1027
|
+
function knownFields(value, allowed) {
|
|
1028
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) throw new Error("Unknown integration field. Credentials and user/workspace IDs are not accepted.");
|
|
1029
|
+
}
|
|
1030
|
+
function containsCredential(value) {
|
|
1031
|
+
return /-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+\S{12,}|\b(?:sk-(?:proj-)?|AIza|cfat_|apikey_|tlk_)[A-Za-z0-9_-]{15,}|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|(?:api[_ -]?key|access[_ -]?token|secret|password)\s*[:=]\s*["']?\S{8,}/i.test(value);
|
|
1032
|
+
}
|
|
1033
|
+
function validateIntegrationUpdate(providerId, input) {
|
|
1034
|
+
const provider = integrationCatalog.find((entry) => entry.id === providerId);
|
|
1035
|
+
if (!provider) throw new Error("Unknown integration provider.");
|
|
1036
|
+
if (!object2(input)) throw new Error("Integration settings must be an object.");
|
|
1037
|
+
knownFields(input, ["enabled", "accessMethod", "preferences", "expectedRevision"]);
|
|
1038
|
+
if (typeof input.enabled !== "boolean") throw new Error("enabled must be a boolean.");
|
|
1039
|
+
if (!provider.accessMethods.includes(input.accessMethod)) throw new Error("Unsupported access method for this provider.");
|
|
1040
|
+
if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) throw new Error("The current expectedRevision is required.");
|
|
1041
|
+
if (!object2(input.preferences)) throw new Error("preferences must be an object.");
|
|
1042
|
+
knownFields(input.preferences, ["instructions", "model", "voiceId", "language", "musicMode", "spending"]);
|
|
1043
|
+
const preferences = {};
|
|
1044
|
+
for (const [field, max] of [["instructions", 1e3], ["model", 120], ["voiceId", 120], ["language", 60]]) {
|
|
1045
|
+
const value = input.preferences[field];
|
|
1046
|
+
if (value === void 0) continue;
|
|
1047
|
+
if (typeof value !== "string" || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value)) throw new Error(`${field} must be text of at most ${max} characters.`);
|
|
1048
|
+
if (containsCredential(value)) throw new Error("Credentials must stay in your provider connection, never in integration preferences.");
|
|
1049
|
+
if (value.trim()) preferences[field] = value.trim();
|
|
1050
|
+
}
|
|
1051
|
+
const musicMode = input.preferences.musicMode, spending = input.preferences.spending;
|
|
1052
|
+
if (musicMode !== void 0) {
|
|
1053
|
+
if (!provider.capabilities.includes("music") || !["instrumental", "vocals", "either"].includes(musicMode)) throw new Error("musicMode is only available for music integrations.");
|
|
1054
|
+
preferences.musicMode = musicMode;
|
|
1055
|
+
}
|
|
1056
|
+
if (preferences.voiceId && !provider.capabilities.includes("voiceover")) throw new Error("voiceId is only available for voice integrations.");
|
|
1057
|
+
if (spending !== void 0 && !["ask", "use-approved-budget"].includes(spending)) throw new Error("Invalid spending preference.");
|
|
1058
|
+
preferences.spending = spending ?? "ask";
|
|
1059
|
+
return { enabled: input.enabled, accessMethod: input.accessMethod, preferences, expectedRevision: input.expectedRevision };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
334
1062
|
// packages/core/src/index.ts
|
|
335
1063
|
var SCHEMA_VERSION = 1;
|
|
336
1064
|
var MAX_DOCUMENT_BYTES = 128 * 1024;
|
|
@@ -345,7 +1073,7 @@ var ValidationError = class extends Error {
|
|
|
345
1073
|
var identifier2 = /^[a-z0-9][a-z0-9_-]{0,79}$/;
|
|
346
1074
|
var tokenName = /^[a-z][a-z0-9_.-]{0,79}$/;
|
|
347
1075
|
var own2 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
348
|
-
var
|
|
1076
|
+
var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
349
1077
|
var nonempty2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
350
1078
|
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
351
1079
|
var tokenValue = (value) => typeof value === "string" || typeof value === "boolean" || finite(value) || Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
@@ -362,13 +1090,13 @@ function safeDocumentPath(path) {
|
|
|
362
1090
|
return path;
|
|
363
1091
|
}
|
|
364
1092
|
function checkRules(value, add) {
|
|
365
|
-
if (!
|
|
1093
|
+
if (!object3(value)) {
|
|
366
1094
|
add("tokens must be an object");
|
|
367
1095
|
return;
|
|
368
1096
|
}
|
|
369
1097
|
for (const [key, rule] of Object.entries(value)) {
|
|
370
1098
|
if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid semantic token ${key}`);
|
|
371
|
-
if (!
|
|
1099
|
+
if (!object3(rule) || !tokenValue(rule.value) || !["required", "preferred", "example"].includes(String(rule.strength))) add(`Token ${key} requires value and strength required/preferred/example`);
|
|
372
1100
|
}
|
|
373
1101
|
}
|
|
374
1102
|
function checkEvidence(value, add) {
|
|
@@ -377,7 +1105,7 @@ function checkEvidence(value, add) {
|
|
|
377
1105
|
return;
|
|
378
1106
|
}
|
|
379
1107
|
for (const ref of value) {
|
|
380
|
-
if (!
|
|
1108
|
+
if (!object3(ref) || !nonempty2(ref.videoId) || !["reference_only", "licensed_asset", "owned_asset"].includes(String(ref.relationship))) add("Evidence must include videoId and explicit relationship");
|
|
381
1109
|
else {
|
|
382
1110
|
if (ref.timestampSeconds !== void 0 && (!finite(ref.timestampSeconds) || ref.timestampSeconds < 0)) add("Evidence timestamp must be nonnegative seconds");
|
|
383
1111
|
if (ref.reviewStatus !== void 0 && !["unreviewed", "agent_reviewed", "human_verified"].includes(String(ref.reviewStatus))) add("Invalid evidence reviewStatus");
|
|
@@ -398,7 +1126,16 @@ function validateDocument(path, content) {
|
|
|
398
1126
|
return { valid: false, issues };
|
|
399
1127
|
}
|
|
400
1128
|
if (content.includes("\0")) add("NUL bytes are not allowed");
|
|
401
|
-
if (path.endsWith(".md"))
|
|
1129
|
+
if (path.endsWith(".md")) {
|
|
1130
|
+
if (/^\.editorbrain\/brands\/[^/]+\/design\.md$/.test(canonicalDocumentPath(path))) {
|
|
1131
|
+
try {
|
|
1132
|
+
readProfile(content);
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
add(error.message);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return { valid: issues.length === 0, issues };
|
|
1138
|
+
}
|
|
402
1139
|
let doc;
|
|
403
1140
|
try {
|
|
404
1141
|
doc = JSON.parse(content);
|
|
@@ -406,7 +1143,7 @@ function validateDocument(path, content) {
|
|
|
406
1143
|
add("Invalid JSON");
|
|
407
1144
|
return { valid: false, issues };
|
|
408
1145
|
}
|
|
409
|
-
if (!
|
|
1146
|
+
if (!object3(doc)) {
|
|
410
1147
|
add("JSON document must be an object");
|
|
411
1148
|
return { valid: false, issues };
|
|
412
1149
|
}
|
|
@@ -430,19 +1167,20 @@ function validateDocument(path, content) {
|
|
|
430
1167
|
if (isStyle) {
|
|
431
1168
|
if (!nonempty2(doc.name)) add("Style name is required");
|
|
432
1169
|
if (doc.references !== void 0) checkEvidence(doc.references, add);
|
|
433
|
-
if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !
|
|
1170
|
+
if (doc.components !== void 0 && (!Array.isArray(doc.components) || doc.components.some((c) => !object3(c) || !nonempty2(c.id) || !["concept", "implemented", "transfer_tested"].includes(String(c.status))))) add("Components need id and explicit implementation status");
|
|
434
1171
|
}
|
|
435
1172
|
}
|
|
436
1173
|
if (isProject) {
|
|
437
1174
|
for (const key of ["id", "brand", "style", "outputProfile"]) if (typeof doc[key] !== "string" || !identifier2.test(doc[key])) add(`${key} must be a valid identifier`);
|
|
438
|
-
if (!
|
|
1175
|
+
if (!object3(doc.intent) || !nonempty2(doc.intent.topic) || !nonempty2(doc.intent.goal)) add("intent requires topic and goal");
|
|
439
1176
|
if (doc.reviewMode !== void 0 && !["direct", "directions", "sample"].includes(doc.reviewMode)) add("reviewMode must be direct, directions or sample");
|
|
1177
|
+
if (doc.mediaStyleOverrides !== void 0) for (const issue of mediaStyleOverrideIssues(doc.mediaStyleOverrides)) add(issue);
|
|
440
1178
|
if (doc.script !== void 0 && typeof doc.script !== "string") add("script must be text");
|
|
441
1179
|
if (doc.overrides !== void 0) {
|
|
442
|
-
if (!
|
|
1180
|
+
if (!object3(doc.overrides)) add("overrides must be an object");
|
|
443
1181
|
else for (const [key, rule] of Object.entries(doc.overrides)) {
|
|
444
1182
|
if (!tokenName.test(key) || ["__proto__", "constructor", "prototype"].includes(key)) add(`Invalid override token ${key}`);
|
|
445
|
-
if (!
|
|
1183
|
+
if (!object3(rule) || !tokenValue(rule.value) || !nonempty2(rule.reason) || rule.overrideRequired !== void 0 && typeof rule.overrideRequired !== "boolean") add(`Override ${key} requires value and a reason; overrideRequired must be boolean`);
|
|
446
1184
|
}
|
|
447
1185
|
}
|
|
448
1186
|
}
|
|
@@ -452,7 +1190,7 @@ function validateDocument(path, content) {
|
|
|
452
1190
|
else {
|
|
453
1191
|
const ids = /* @__PURE__ */ new Set();
|
|
454
1192
|
for (const asset of doc.assets) {
|
|
455
|
-
if (!
|
|
1193
|
+
if (!object3(asset) || typeof asset.id !== "string" || !/^[a-f0-9]{64}$/.test(asset.id) || !nonempty2(asset.name) || typeof asset.mimeType !== "string" || !/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9.+-]*$/.test(asset.mimeType) || !Number.isSafeInteger(asset.bytes) || Number(asset.bytes) < 1) {
|
|
456
1194
|
add("Production assets require a SHA-256 id, name, media mimeType and positive integer bytes");
|
|
457
1195
|
continue;
|
|
458
1196
|
}
|
|
@@ -466,7 +1204,7 @@ function validateDocument(path, content) {
|
|
|
466
1204
|
for (const key of ["width", "height"]) if (!Number.isInteger(doc[key]) || Number(doc[key]) < 1 || Number(doc[key]) > 16384) add(`${key} must be an integer between 1 and 16384`);
|
|
467
1205
|
if (!finite(doc.fps) || doc.fps <= 0 || doc.fps > 240) add("fps must be between 0 and 240");
|
|
468
1206
|
if (doc.safeAreas !== void 0) {
|
|
469
|
-
if (!
|
|
1207
|
+
if (!object3(doc.safeAreas) || ["top", "right", "bottom", "left"].some((k) => !finite(doc.safeAreas[k]) || Number(doc.safeAreas[k]) < 0 || Number(doc.safeAreas[k]) >= 0.5)) add("safeAreas must have normalized fractions from 0 up to (not including) 0.5 for all four sides");
|
|
470
1208
|
}
|
|
471
1209
|
}
|
|
472
1210
|
return { valid: issues.length === 0, issues };
|
|
@@ -501,7 +1239,7 @@ function validateWorkspace(files) {
|
|
|
501
1239
|
return { valid: issues.length === 0, issues };
|
|
502
1240
|
}
|
|
503
1241
|
function stableJson(value) {
|
|
504
|
-
const sort = (item) => Array.isArray(item) ? item.map(sort) :
|
|
1242
|
+
const sort = (item) => Array.isArray(item) ? item.map(sort) : object3(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, sort(item[key])])) : item;
|
|
505
1243
|
return JSON.stringify(sort(value), null, 2) + "\n";
|
|
506
1244
|
}
|
|
507
1245
|
async function sha256(content) {
|
|
@@ -554,6 +1292,7 @@ async function resolveProject(files, projectId) {
|
|
|
554
1292
|
if (!files[directionPath].trim() || files[directionPath].includes("DIRECTION_PENDING")) warnings.push("Creative direction is pending: the host agent or editor must write a coherent plan for the actual material.");
|
|
555
1293
|
for (const component of style.components ?? []) if (component.status !== "transfer_tested") warnings.push(`Component ${component.id} is ${component.status}; transfer fidelity is not verified.`);
|
|
556
1294
|
const sourceHashes = Object.fromEntries(await Promise.all(sourcePaths.map(async (source) => [source, await sha256(files[source])])));
|
|
1295
|
+
const editorialProfile = readProfile(files[optionalPaths[0]] ?? "");
|
|
557
1296
|
return {
|
|
558
1297
|
schemaVersion: 1,
|
|
559
1298
|
project,
|
|
@@ -568,6 +1307,7 @@ async function resolveProject(files, projectId) {
|
|
|
568
1307
|
...own2(files, optionalPaths[3]) ? { media: JSON.parse(files[optionalPaths[3]]) } : {},
|
|
569
1308
|
...own2(files, optionalPaths[7]) ? { styleResources: JSON.parse(files[optionalPaths[7]]) } : {},
|
|
570
1309
|
memory: resolveMemory(files, project),
|
|
1310
|
+
mediaPreferences: resolveMediaPreferences(editorialProfile.mediaStyles, project.mediaStyleOverrides, editorialProfile.visualTaste, editorialProfile.motionSelection, editorialProfile.motionProduction, editorialProfile.editorialDelivery, editorialProfile.captions === "Sin subt\xEDtulos" || output2.captions?.enabled === false ? false : void 0),
|
|
571
1311
|
evidence: style.references ?? [],
|
|
572
1312
|
warnings,
|
|
573
1313
|
lock: { schemaVersion: 1, projectId, sourceHashes, overrides: project.overrides ?? {}, adapters: Object.fromEntries(["neutral", "hyperframes", "remotion"].map((key) => [key, { version: "0.2.1", kind: "editorial_handoff", rendered: false }])) }
|
|
@@ -589,6 +1329,10 @@ ${stableJson(resolved.media)}\`\`\``) : "";
|
|
|
589
1329
|
|
|
590
1330
|
\`\`\`json
|
|
591
1331
|
${stableJson(resolved.styleResources)}\`\`\``) : "";
|
|
1332
|
+
const mediaStyleSection = resolved.mediaPreferences.editorialDelivery || resolved.mediaPreferences.motionSelectionSource !== "implicit" || resolved.mediaPreferences.motionProductionSource !== "implicit" || Object.keys(resolved.mediaPreferences.taste).length || resolved.mediaPreferences.image.selections.length || resolved.mediaPreferences.motion.selections.length || resolved.mediaPreferences.image.mode !== "inherit" || resolved.mediaPreferences.motion.mode !== "inherit" ? section("Media style preferences", `Adaptive motion: choose per scene, using favorites as guidance and excluding avoided styles; no animation is also valid. Guided motion: start from saved choices, checking scene fit. Production auto/code/ai is separate: code can build complete JavaScript scenes; ai prefers generated clips only with verified personal tool access and existing authorization. Disabled categories remain disabled. Retrieve only relevant recipes by ID and version. Required brand tokens govern.
|
|
1333
|
+
|
|
1334
|
+
\`\`\`json
|
|
1335
|
+
${stableJson(resolved.mediaPreferences)}\`\`\``) : "";
|
|
592
1336
|
if (adapter === "neutral") return { ...common, "brief.md": `# ${resolved.project.id}
|
|
593
1337
|
|
|
594
1338
|
${disclaimer}
|
|
@@ -605,7 +1349,7 @@ ${resolved.editorial.brand}
|
|
|
605
1349
|
|
|
606
1350
|
## Style
|
|
607
1351
|
|
|
608
|
-
${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
|
|
1352
|
+
${resolved.editorial.style}${section("Design", resolved.editorial.design)}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}${mediaStyleSection}
|
|
609
1353
|
|
|
610
1354
|
Exact tokens, output settings and evidence are in resolved.json.
|
|
611
1355
|
` };
|
|
@@ -633,13 +1377,13 @@ ${resolved.editorial.direction}
|
|
|
633
1377
|
|
|
634
1378
|
## Style intent
|
|
635
1379
|
|
|
636
|
-
${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}
|
|
1380
|
+
${resolved.editorial.style}${section("Effects", resolved.editorial.effects)}${section("Music and sound", resolved.editorial.music)}${mediaSection}${resourcesSection}${mediaStyleSection}
|
|
637
1381
|
|
|
638
1382
|
## Delivery
|
|
639
1383
|
|
|
640
1384
|
${resolved.output.width} \xD7 ${resolved.output.height}, ${resolved.output.fps} fps. Adapt composition to the actual material and safe areas in resolved.json. Implement and preview the composition in Hyperframes before claiming completion.
|
|
641
1385
|
` };
|
|
642
|
-
if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, ...resolved.media ? { media: resolved.media } : {}, ...resolved.styleResources ? { styleResources: resolved.styleResources } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
|
|
1386
|
+
if (adapter === "remotion") return { ...common, "remotion-plan.json": stableJson({ schemaVersion: 1, kind: "editorial_handoff", renderer: "remotion", executable: false, note: disclaimer, composition: { width: resolved.output.width, height: resolved.output.height, fps: resolved.output.fps, durationInFrames: null }, tokens: resolved.tokens, project: resolved.project, direction: resolved.editorial.direction, editorial: resolved.editorial, mediaPreferences: resolved.mediaPreferences, ...resolved.media ? { media: resolved.media } : {}, ...resolved.styleResources ? { styleResources: resolved.styleResources } : {}, evidence: resolved.evidence, implementationTasks: ["Determine duration from the script/audio and actual cuts.", "Implement React composition and assets.", "Preview and verify typography, safe areas, motion and audio."] }) };
|
|
643
1387
|
throw new Error(`Unsupported adapter: ${adapter}`);
|
|
644
1388
|
}
|
|
645
1389
|
function createWorkspace(options = {}) {
|
|
@@ -1116,7 +1860,7 @@ import { createHash } from "node:crypto";
|
|
|
1116
1860
|
import { constants as constants2 } from "node:fs";
|
|
1117
1861
|
import { lstat as lstat2, open as open2, readdir as readdir2, rename as rename2 } from "node:fs/promises";
|
|
1118
1862
|
import { fileURLToPath } from "node:url";
|
|
1119
|
-
var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3", "f18269ad5504fff8a4fc522d463483261f5041e56cb7f1081f9614bc67e0d96f"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd", "faa2bba99ae3023be7f588d02177a4abf583d3a8a65f617b8cff7a3ceed09dfc"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05", "490daf154b428b536d0a6f590030c854b2fc8ca6adb995c6ecd5a162f85e471e"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781", "b86eae85cf86dbf4409b10f4d8df1f1649fc408ce1a240f7cf26d5bb69aabfd1"], "references/first-edit.md": ["cb1e9d3272b2b22ae095aed02e33067c646da48e7b64cf71ff81197f6e67b673"], "references/memory-and-review.md": ["51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289"], "references/search-brief.md": ["137203383a7873593136428520f5f9cd6ce1ab6cfeab365c277175786eba3ab3"], "references/style-creator.md": ["1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2"] };
|
|
1863
|
+
var previousSkillHashes = { "SKILL.md": ["9df2c89deeec30ddf556c359027efe1d90e6b4e9c08717cd8827e210b38a187f", "67b157cb45cae3621e6f2c43235c1d69907ad6abd45ab6b52a3c65f236732db9", "1e2fff65a1b303b18b2d65156bef6eb2439a19d77ba2a25b37bcb1819ff009e3", "dacce1c4127f4281df6f7e1681e243444bb6f8a8e74b829e52ee0108c6672348", "09740ab4b275eada65b5040ae6000104ff39094f534a2784e67feb2afc2a59b3", "f18269ad5504fff8a4fc522d463483261f5041e56cb7f1081f9614bc67e0d96f", "5779a28879313070b0d819771ac967d4a133cff1c3dbda2ec44b4dd2abcb17d1", "30f1e64622e70552a1a6129cacc646f56b09b3d7a1fd3f5bf22299bc6bfd0d1e", "6975d917226f75ad55c67543bf7f1c5ef18f4cfe43426c69400c56c1ed497db3"], "AGENTS.md": ["4f3ef860ade3a31aa7c6a6aab5cf6ec2a700850bf19ea2320324c12c2e0fbf5b", "1b07de01698a0040b47ea0eddb1b017f70a69e81acd0bf9710647ca5e02f5125", "b5ee3439ff974c668755816149fab76899d7ad9266f63978b30fbc9b7d2cd640", "bacedb7bdedab5abe75c9601717cfdb05a96208fa2e2a42af878e2dfd2ae4251", "ea3b473179857ccc07e9e4f21a6ede9e0906a09e495f4a051069d15aee1556cd", "faa2bba99ae3023be7f588d02177a4abf583d3a8a65f617b8cff7a3ceed09dfc", "83c75394bc7eaab6662a76d89339361e836cc1a988237720eeeacdd6b62e726f", "b685cb7ff21d5bfe19d96ce0139c17b7eb014b7ecda8ec5321558b0004bfdc71"], "references/cli-mcp.md": ["5d3a79343fbd76cdae5bdbe12fb9c944d5dff4f489ac6534fd96e0cb8630b988", "8e011c31368cef2117a28ee972b39aab9535eeb0f601a4a0f9d6fc019b22c1c2", "666082653efc000f8a7cc5f1836c80bbdda5f0f57cdbbdcb393f2b84aeb93a01", "78b737796a8685b1135e0acb91b620c51845b62e31dba04eb51dd9dd38b41008", "8b2e01bfd937237919f5ade794eed01dc6e32b556ede9de3a0ccc28cbdd988d1", "ca88b2816a795a5ebe834074b178469367634b80ad837e3697182ea585af1c05", "490daf154b428b536d0a6f590030c854b2fc8ca6adb995c6ecd5a162f85e471e", "9f98affecd69752c17f52fb51ae74b26a1cadea5919b8b74b7865a20661c4418", "19fc85e92cef203f4162e2bfb2dd2df8ef90472bc8f81dfb568b65a714335ff9"], "references/production-and-review.md": ["055c6df9e0f167be148b0a5c0bba5b43e108eb1f58f478d88b3536c6589ac7b3", "4a4035b87a6c024b419847ab8592bddfeb9b2d9c184f65880c47d661f0bb250e"], "references/workspace-files.md": ["e72af2871f42cd776be1bfff8009ed5562780e7f8a65b12c27887f3880631f81", "00780c64bd0244c6afb18d786b7512b43e315c376e8f88c67d6ba53d91275ccf", "97053b092e0ff36e3cfb764095fcb4a4fca94e722567e774b53bc118279f40e5", "70fc660e7ed3d70c16c33c62c8e0399b99c3251808f093ef3d4ea5e8d39e4c8a", "28c62c1e28842142607f5168ee79b18fd797900dd66dd9ac5eb3cfcf8a09f781", "b86eae85cf86dbf4409b10f4d8df1f1649fc408ce1a240f7cf26d5bb69aabfd1", "fc65f343e50b91327653bdc5d94f35af9c86c097bb46810b4b2f7c9a1c200166"], "references/first-edit.md": ["cb1e9d3272b2b22ae095aed02e33067c646da48e7b64cf71ff81197f6e67b673", "5a02253d9907f5e6705304565da7b2d1787e84ae17616414b295a8526ec3e4a5"], "references/memory-and-review.md": ["51a274e522270f1fc295d0c0b9a1c23672ce01abea097c2499c9a8db56c0c289"], "references/search-brief.md": ["137203383a7873593136428520f5f9cd6ce1ab6cfeab365c277175786eba3ab3", "df93154562bb89a9f22d31260c17406565f5f308c407643c85d15e528ef2c086"], "references/style-creator.md": ["1b1bc289a5bc308cbbf7f4d699de2933b8934733a2b69b3e98fb1b7420d986f2"], "references/local-production.md": ["0a13c65cd60e40ba703095f5e33a1ea66433323b38345c2034fb73c602c438ee"], "references/integrations.md": ["a3500dd79c71ab3ee80a865bb5bf8c94287efb933e07d577738891e276a819a0"], "references/media-preferences.md": ["970b97fbd55b8e9e05fd7e51b4a953cd1b96b9bee7830382aa7514f1b240b3aa", "481c643a5e6516413a6cae970b5170573c981d0effd7d4e146c5cd07c0ee3b1f", "8f11e1956ed06ab73ba58d8c880f0d2292eb76829b07d2de2eaaed1965885158", "e94bc13c05ffd57eceecc4ceaa42df2b83904c12f8c552e161c98f13e6427e5b"] };
|
|
1120
1864
|
var destinations = {
|
|
1121
1865
|
codex: { directory: ".agents/skills/editorbrain", entryPoint: "AGENTS.md" },
|
|
1122
1866
|
claude: { directory: ".claude/skills/editorbrain", entryPoint: "CLAUDE.md" }
|
|
@@ -1478,8 +2222,8 @@ async function setup(options, runner = runHost) {
|
|
|
1478
2222
|
}
|
|
1479
2223
|
|
|
1480
2224
|
// packages/cli/src/index.ts
|
|
1481
|
-
import { chmod, lstat as lstat7, mkdir as mkdir5, open as
|
|
1482
|
-
import { constants as
|
|
2225
|
+
import { chmod, lstat as lstat7, mkdir as mkdir5, open as open8 } from "node:fs/promises";
|
|
2226
|
+
import { constants as constants8 } from "node:fs";
|
|
1483
2227
|
import { homedir } from "node:os";
|
|
1484
2228
|
import { basename as basename2, dirname as dirname6, extname as extname3, join as join3, resolve as resolve6 } from "node:path";
|
|
1485
2229
|
|
|
@@ -1520,7 +2264,7 @@ function expired() {
|
|
|
1520
2264
|
function invalid() {
|
|
1521
2265
|
return new DeviceLoginError("invalid_response", "The authorization server returned an invalid response. No credential was saved.");
|
|
1522
2266
|
}
|
|
1523
|
-
function
|
|
2267
|
+
function record4(value) {
|
|
1524
2268
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1525
2269
|
}
|
|
1526
2270
|
function abortCheck(signal) {
|
|
@@ -1568,11 +2312,11 @@ async function responseJson(response) {
|
|
|
1568
2312
|
}
|
|
1569
2313
|
}
|
|
1570
2314
|
function sessionFrom(value) {
|
|
1571
|
-
if (!
|
|
2315
|
+
if (!record4(value) || typeof value.deviceCode !== "string" || !/^[a-zA-Z0-9._~-]{16,1024}$/.test(value.deviceCode) || typeof value.userCode !== "string" || !/^[A-Z0-9-]{4,32}$/.test(value.userCode) || typeof value.verificationUri !== "string" || !Number.isInteger(value.expiresIn) || Number(value.expiresIn) < 1 || Number(value.expiresIn) > 3600 || !Number.isInteger(value.interval) || Number(value.interval) < 1 || Number(value.interval) > 60) throw invalid();
|
|
1572
2316
|
return { deviceCode: value.deviceCode, userCode: value.userCode, verificationUri: verificationUrl(value.verificationUri, value.userCode, value.deviceCode), expiresIn: Number(value.expiresIn), interval: Number(value.interval) };
|
|
1573
2317
|
}
|
|
1574
2318
|
function authorizationFrom(value) {
|
|
1575
|
-
if (!
|
|
2319
|
+
if (!record4(value) || typeof value.apiKey !== "string" || !/^eh_[a-f0-9]{64}$/.test(value.apiKey) || value.role !== "editor" || !record4(value.workspace) || typeof value.workspace.id !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(value.workspace.id) || typeof value.workspace.name !== "string" || !value.workspace.name.trim() || value.workspace.name.length > 100) throw invalid();
|
|
1576
2320
|
return { apiKey: value.apiKey, workspace: { id: value.workspace.id, name: value.workspace.name }, role: "editor" };
|
|
1577
2321
|
}
|
|
1578
2322
|
async function loginWithDevice(options, overrides = {}) {
|
|
@@ -2249,21 +2993,21 @@ import { resolve as resolve5 } from "node:path";
|
|
|
2249
2993
|
function invalidProfile(message) {
|
|
2250
2994
|
throw new CliError("RENDER_PROFILE_INVALID", message, "Use a schemaVersion: 1 render profile with explicit technical requirements.");
|
|
2251
2995
|
}
|
|
2252
|
-
function
|
|
2996
|
+
function object4(value, path, keys) {
|
|
2253
2997
|
if (!value || typeof value !== "object" || Array.isArray(value)) invalidProfile(`${path} must be an object.`);
|
|
2254
2998
|
const result = value;
|
|
2255
2999
|
for (const key of Object.keys(result)) if (!keys.includes(key)) invalidProfile(`${path} contains unsupported field ${key}.`);
|
|
2256
3000
|
return result;
|
|
2257
3001
|
}
|
|
2258
3002
|
function validateRenderCheckProfile(value) {
|
|
2259
|
-
const root =
|
|
3003
|
+
const root = object4(value, "profile", ["schemaVersion", "expected", "tolerance", "audio", "silence", "black", "peak", "limits"]);
|
|
2260
3004
|
if (root.schemaVersion !== 1) invalidProfile("profile.schemaVersion must be 1.");
|
|
2261
|
-
const expected = root.expected === void 0 ? {} :
|
|
2262
|
-
const tolerance = root.tolerance === void 0 ? {} :
|
|
2263
|
-
const silence = root.silence === void 0 ? {} :
|
|
2264
|
-
const black = root.black === void 0 ? {} :
|
|
2265
|
-
const peak = root.peak === void 0 ? {} :
|
|
2266
|
-
const limits = root.limits === void 0 ? {} :
|
|
3005
|
+
const expected = root.expected === void 0 ? {} : object4(root.expected, "expected", ["width", "height", "fps", "durationSeconds"]);
|
|
3006
|
+
const tolerance = root.tolerance === void 0 ? {} : object4(root.tolerance, "tolerance", ["fps", "durationSeconds"]);
|
|
3007
|
+
const silence = root.silence === void 0 ? {} : object4(root.silence, "silence", ["enabled", "minDurationSeconds", "thresholdDb"]);
|
|
3008
|
+
const black = root.black === void 0 ? {} : object4(root.black, "black", ["enabled", "minDurationSeconds", "pixelThreshold", "pictureBlackRatio"]);
|
|
3009
|
+
const peak = root.peak === void 0 ? {} : object4(root.peak, "peak", ["enabled", "thresholdDbFS"]);
|
|
3010
|
+
const limits = root.limits === void 0 ? {} : object4(root.limits, "limits", ["timeoutMs", "maxOutputBytes"]);
|
|
2267
3011
|
const number = (v, name, min, max, fallback, integer = false) => {
|
|
2268
3012
|
if (v === void 0 && fallback !== void 0) return fallback;
|
|
2269
3013
|
if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max || integer && !Number.isInteger(v)) invalidProfile(`${name} must be ${integer ? "an integer" : "a finite number"} between ${min} and ${max}.`);
|
|
@@ -2355,11 +3099,11 @@ function readDetectorOutput(report, stderr, audioStreams) {
|
|
|
2355
3099
|
const match = /black_start:([\d.e+-]+)\s+black_end:([\d.e+-]+)\s+black_duration:([\d.e+-]+)/i.exec(line);
|
|
2356
3100
|
if (match && match.slice(1).every((value) => Number.isFinite(Number(value)))) addFinding(report, { kind: "black_candidate", streamIndex, startSeconds: Number(match[1]), endSeconds: Number(match[2]), durationSeconds: Number(match[3]), requiresReview: true, message: "A mostly black interval met the configured detector threshold. It may be intentional; review in context." });
|
|
2357
3101
|
} else if (tag[1] === "silencedetect@a") {
|
|
2358
|
-
const
|
|
2359
|
-
if (
|
|
2360
|
-
const
|
|
2361
|
-
if (
|
|
2362
|
-
addFinding(report, { kind: "silence_candidate", streamIndex, startSeconds: pendingSilence.get(streamIndex) ?? Number(
|
|
3102
|
+
const start2 = /silence_start:\s*([\d.e+-]+)/i.exec(line);
|
|
3103
|
+
if (start2 && Number.isFinite(Number(start2[1]))) pendingSilence.set(streamIndex, Number(start2[1]));
|
|
3104
|
+
const end2 = /silence_end:\s*([\d.e+-]+)\s*\|\s*silence_duration:\s*([\d.e+-]+)/i.exec(line);
|
|
3105
|
+
if (end2 && end2.slice(1).every((value) => Number.isFinite(Number(value)))) {
|
|
3106
|
+
addFinding(report, { kind: "silence_candidate", streamIndex, startSeconds: pendingSilence.get(streamIndex) ?? Number(end2[1]) - Number(end2[2]), endSeconds: Number(end2[1]), durationSeconds: Number(end2[2]), requiresReview: true, message: "A quiet interval met the configured detector threshold. Pauses and silent edits may be intentional." });
|
|
2363
3107
|
pendingSilence.delete(streamIndex);
|
|
2364
3108
|
}
|
|
2365
3109
|
} else {
|
|
@@ -2579,6 +3323,97 @@ function summarizeProductionReport(value) {
|
|
|
2579
3323
|
throw new CliError("INVALID_REPORT", "Unknown production report kind.", "Use media inventory/sample or production check/plan-check.");
|
|
2580
3324
|
}
|
|
2581
3325
|
|
|
3326
|
+
// packages/cli/src/integrations.ts
|
|
3327
|
+
import { constants as constants7 } from "node:fs";
|
|
3328
|
+
import { open as open7 } from "node:fs/promises";
|
|
3329
|
+
function object5(value) {
|
|
3330
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3331
|
+
}
|
|
3332
|
+
function invalidState(write) {
|
|
3333
|
+
return new CliError(
|
|
3334
|
+
"INVALID_RESPONSE",
|
|
3335
|
+
write ? "The integration write could not be confirmed from the API response." : "The API returned invalid integration state.",
|
|
3336
|
+
write ? "The write may have committed. Run editorbrain integrations list for this endpoint and reconcile remote state before another write." : "Run editorbrain integrations list again for this endpoint; check the service if the response remains invalid.",
|
|
3337
|
+
false,
|
|
3338
|
+
{ outcome: write ? "unknown" : "read_failed" }
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
function integrationState(value, write = false) {
|
|
3342
|
+
if (!object5(value) || !["member", "connection"].includes(value.scope) || !Number.isSafeInteger(value.revision) || value.revision < 0 || !(value.updatedAt === null || typeof value.updatedAt === "string" && value.updatedAt.length <= 64 && Number.isFinite(Date.parse(value.updatedAt))) || !Array.isArray(value.entries) || !Array.isArray(value.catalog)) throw invalidState(write);
|
|
3343
|
+
const providers = /* @__PURE__ */ new Set();
|
|
3344
|
+
for (const entry of value.entries) {
|
|
3345
|
+
if (!object5(entry) || typeof entry.providerId !== "string" || !entry.providerId || entry.providerId.length > 120 || providers.has(entry.providerId) || typeof entry.enabled !== "boolean" || typeof entry.accessMethod !== "string" || !entry.accessMethod || entry.accessMethod.length > 64 || !object5(entry.preferences)) throw invalidState(write);
|
|
3346
|
+
providers.add(entry.providerId);
|
|
3347
|
+
}
|
|
3348
|
+
if (value.catalog.some((provider) => !object5(provider) || typeof provider.id !== "string" || !provider.id)) throw invalidState(write);
|
|
3349
|
+
return {
|
|
3350
|
+
scope: value.scope,
|
|
3351
|
+
revision: value.revision,
|
|
3352
|
+
updatedAt: value.updatedAt,
|
|
3353
|
+
entries: value.entries,
|
|
3354
|
+
catalog: value.catalog
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
async function readIntegrationSettings(file) {
|
|
3358
|
+
const handle = await open7(file, constants7.O_RDONLY | constants7.O_NOFOLLOW | constants7.O_NONBLOCK);
|
|
3359
|
+
try {
|
|
3360
|
+
const stat = await handle.stat();
|
|
3361
|
+
if (!stat.isFile() || stat.size > 16 * 1024) throw new CliError("INVALID_ARGUMENT", "Choose a regular integration settings JSON file under 16 KiB.", "Store provider credentials in the provider connection, not this file.");
|
|
3362
|
+
try {
|
|
3363
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
3364
|
+
} catch {
|
|
3365
|
+
throw new CliError("INVALID_ARGUMENT", "Integration settings must be valid JSON.", "The file should contain enabled, accessMethod and preferences only.");
|
|
3366
|
+
}
|
|
3367
|
+
} finally {
|
|
3368
|
+
await handle.close();
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
function integrationRevision(value) {
|
|
3372
|
+
if (value === void 0 || !/^(0|[1-9]\d*)$/.test(value) || !Number.isSafeInteger(Number(value))) {
|
|
3373
|
+
throw new CliError("INVALID_ARGUMENT", "--expected-revision must be the revision returned by integrations list.", "Read current settings before changing them.");
|
|
3374
|
+
}
|
|
3375
|
+
return Number(value);
|
|
3376
|
+
}
|
|
3377
|
+
async function listIntegrations(client, details = false) {
|
|
3378
|
+
const state = integrationState(await client.request("/v1/integrations"));
|
|
3379
|
+
if (details) return state;
|
|
3380
|
+
return {
|
|
3381
|
+
scope: state.scope,
|
|
3382
|
+
revision: state.revision,
|
|
3383
|
+
updatedAt: state.updatedAt,
|
|
3384
|
+
entries: state.entries,
|
|
3385
|
+
access: "user_declared",
|
|
3386
|
+
notice: "Verify provider access in this agent session before use. Settings do not connect tools or authorize charges."
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
async function setIntegration(client, providerId, settings, expectedRevision) {
|
|
3390
|
+
let update;
|
|
3391
|
+
try {
|
|
3392
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings) || "expectedRevision" in settings) throw new Error();
|
|
3393
|
+
update = validateIntegrationUpdate(providerId, { ...settings, expectedRevision });
|
|
3394
|
+
} catch {
|
|
3395
|
+
throw new CliError("INVALID_ARGUMENT", "Invalid integration preferences, access method or provider.", "Use integrations catalog for supported providers. The JSON accepts enabled, accessMethod, preferences; never credentials.");
|
|
3396
|
+
}
|
|
3397
|
+
let response;
|
|
3398
|
+
try {
|
|
3399
|
+
response = await client.request(`/v1/integrations/${encodeURIComponent(providerId)}`, { method: "PUT", body: update });
|
|
3400
|
+
} catch (error) {
|
|
3401
|
+
if (error instanceof ApiError && error.status === 409) throw new CliError(
|
|
3402
|
+
"INTEGRATION_CONFLICT",
|
|
3403
|
+
"Integration preferences changed before this save.",
|
|
3404
|
+
"Run editorbrain integrations list for this endpoint, reconcile the preferences and use the returned revision. Do not retry the old write blindly.",
|
|
3405
|
+
false,
|
|
3406
|
+
{ status: 409 }
|
|
3407
|
+
);
|
|
3408
|
+
if (error instanceof CliError && ["INVALID_RESPONSE", "RESPONSE_TOO_LARGE"].includes(error.code)) throw invalidState(true);
|
|
3409
|
+
throw error;
|
|
3410
|
+
}
|
|
3411
|
+
const state = integrationState(response, true);
|
|
3412
|
+
const entry = state.entries.find((value) => value.providerId === providerId);
|
|
3413
|
+
if (state.revision !== expectedRevision + 1 || !entry || entry.enabled !== update.enabled || entry.accessMethod !== update.accessMethod || stableJson(entry.preferences) !== stableJson(update.preferences)) throw invalidState(true);
|
|
3414
|
+
return { scope: state.scope, revision: state.revision, updatedAt: state.updatedAt, entry, access: "user_declared" };
|
|
3415
|
+
}
|
|
3416
|
+
|
|
2582
3417
|
// packages/cli/src/index.ts
|
|
2583
3418
|
var HELP = `EditorBrain \u2014 portable editorial context for AI video agents
|
|
2584
3419
|
|
|
@@ -2610,7 +3445,10 @@ editorbrain improvements accept RUN --index N --expected-revision N
|
|
|
2610
3445
|
editorbrain review list --project ID
|
|
2611
3446
|
editorbrain review create --project ID --asset SHA --title TEXT --kind sample|render --days 7 --share-authorized
|
|
2612
3447
|
editorbrain review revoke ID
|
|
2613
|
-
editorbrain workflow check [--project id] [--previous-version hash]
|
|
3448
|
+
editorbrain workflow check [--project id] [--previous-version hash] [--previous-context-version hash]
|
|
3449
|
+
editorbrain integrations catalog
|
|
3450
|
+
editorbrain integrations list [--details]
|
|
3451
|
+
editorbrain integrations set <provider-id> --file settings.json --expected-revision N
|
|
2614
3452
|
editorbrain search brief --file <editorial-brief.json> [--project id] [--mode styles|events|both] [--review direct|directions|sample]
|
|
2615
3453
|
editorbrain search edit [--script file | --video file | --asset sha256 | --project id] [--requirements text] [--assets tag,tag] [--mode styles|events|both] [--review direct|directions|sample]
|
|
2616
3454
|
editorbrain library search [query]
|
|
@@ -2621,6 +3459,8 @@ editorbrain narrative get <video-id> [--sha256 document-hash] [--event-offset 0]
|
|
|
2621
3459
|
editorbrain styles list
|
|
2622
3460
|
editorbrain styles create <style-id> [--name name] [--dir folder]
|
|
2623
3461
|
editorbrain styles check <style-id> [--dir folder]
|
|
3462
|
+
editorbrain media styles [--category image|motion]
|
|
3463
|
+
editorbrain media style <style-id> [--style-version 1.0.0]
|
|
2624
3464
|
editorbrain media inventory --input file-or-folder [--max-files 200] [--dir folder] [--out .editorbrain-local/report.json] [--details]
|
|
2625
3465
|
editorbrain media sample --file source --at 0,5,12 --out .editorbrain-local/frames [--dir folder] [--details]
|
|
2626
3466
|
editorbrain production check --file render [--profile local-profile.json] [--dir folder] [--out .editorbrain-local/report.json] [--details]
|
|
@@ -2641,6 +3481,7 @@ Push/pull preserve conflicting changes; media uploads use the separate assets co
|
|
|
2641
3481
|
Skill install works offline, preserves existing instructions, and defaults to both agents.
|
|
2642
3482
|
Styles create/check work locally without login or media upload; checks do not certify render quality.
|
|
2643
3483
|
Media/production commands run locally with ffprobe/ffmpeg, without automatic installation or media upload.
|
|
3484
|
+
Integrations store personal preferences, not credentials. Declared access is checked by your agent before use.
|
|
2644
3485
|
Production output is compact by default; --details expands it. --out saves full private reports without overwriting.
|
|
2645
3486
|
Keys create stores the returned secret in the private user config; stdout has no key.
|
|
2646
3487
|
Output is JSON except help; login progress/code goes to stderr. --url overrides the configured endpoint.
|
|
@@ -2649,7 +3490,7 @@ function parseArgs(argv) {
|
|
|
2649
3490
|
const positional = [];
|
|
2650
3491
|
const flags = {};
|
|
2651
3492
|
const booleans = /* @__PURE__ */ new Set(["help", "version", "offline", "dry-run", "key-stdin", "invite-stdin", "no-browser", "skip-mcp", "all", "share-authorized", "details"]);
|
|
2652
|
-
const known = /* @__PURE__ */ new Set(["input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
|
|
3493
|
+
const known = /* @__PURE__ */ new Set(["input", "max-files", "at", "profile", "inventory", "plan", "dir", "brand", "style", "project", "topic", "goal", "name", "adapter", "out", "url", "document", "mime", "label", "role", "limit", "cursor", "timeout", "assets", "capabilities", "duration", "agent", "workspace", "prefix", "sha256", "kind", "channel", "epistemic-status", "event-offset", "relation-offset", "script", "video", "asset", "requirements", "mode", "review", "previous-version", "previous-context-version", "category", "style-version", "file", "revision", "expected-revision", "before", "scope", "id", "text", "conditions", "status", "index", "title", "days", ...booleans]);
|
|
2653
3494
|
for (let i = 0; i < argv.length; i++) {
|
|
2654
3495
|
const arg = argv[i];
|
|
2655
3496
|
if (!arg.startsWith("--")) {
|
|
@@ -2807,7 +3648,39 @@ async function main() {
|
|
|
2807
3648
|
process.stdout.write(HELP);
|
|
2808
3649
|
return;
|
|
2809
3650
|
}
|
|
2810
|
-
if (args.flags.details && !["media", "production"].includes(command)) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media and
|
|
3651
|
+
if (args.flags.details && !["media", "production", "integrations"].includes(command)) throw new CliError("INVALID_ARGUMENT", "--details is supported only by media, production and integrations list.", "Read editorbrain help.");
|
|
3652
|
+
if (command === "integrations") {
|
|
3653
|
+
const allowed = new Set(subcommand === "catalog" ? [] : subcommand === "list" ? ["url", "details"] : subcommand === "set" ? ["url", "file", "expected-revision", "dir"] : []);
|
|
3654
|
+
if (!["catalog", "list", "set"].includes(subcommand) || args.positional.length > (subcommand === "set" ? 3 : 2)) throw new CliError("INVALID_ARGUMENT", "Use integrations catalog, list or set <provider-id>.", "Read editorbrain help.");
|
|
3655
|
+
for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported for this integrations command.`, "Read editorbrain help.");
|
|
3656
|
+
if (subcommand === "catalog") {
|
|
3657
|
+
output({ catalog: integrationCatalog, credentialsRequired: false });
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (subcommand === "set") {
|
|
3661
|
+
const file = str(args, "file");
|
|
3662
|
+
if (!argument || !file) throw new CliError("INVALID_ARGUMENT", "integrations set requires a provider ID and --file.", "Use integrations list first, then supply --expected-revision.");
|
|
3663
|
+
const revision = integrationRevision(str(args, "expected-revision"));
|
|
3664
|
+
const settings = await readIntegrationSettings(resolve6(str(args, "dir") ?? process.cwd(), file));
|
|
3665
|
+
output(await setIntegration(new Client(await config(args)), argument, settings, revision));
|
|
3666
|
+
} else output(await listIntegrations(new Client(await config(args)), !!args.flags.details));
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
if (command === "media" && (subcommand === "styles" || subcommand === "style")) {
|
|
3670
|
+
const allowed = new Set(subcommand === "styles" ? ["category"] : ["style-version"]);
|
|
3671
|
+
for (const flag of Object.keys(args.flags)) if (!allowed.has(flag)) throw new CliError("INVALID_ARGUMENT", `--${flag} is not supported.`, "Read editorbrain help.");
|
|
3672
|
+
if (subcommand === "styles") {
|
|
3673
|
+
const category = str(args, "category");
|
|
3674
|
+
if (argument || category && !["image", "motion"].includes(category)) throw new CliError("INVALID_ARGUMENT", "Use --category image or motion.", "Read editorbrain media styles.");
|
|
3675
|
+
output({ id: MEDIA_STYLE_CATALOG.id, version: MEDIA_STYLE_CATALOG.version, styles: MEDIA_STYLE_CATALOG.styles.filter((s) => !category || s.category === category).map(({ id, version, category: category2, name, description, status }) => ({ id, version, category: category2, name, description, status })), next: "media style <id> --style-version <version>" });
|
|
3676
|
+
} else {
|
|
3677
|
+
if (!argument) throw new CliError("INVALID_ARGUMENT", "A media style id is required.", "Use editorbrain media styles.");
|
|
3678
|
+
const style = getMediaStyle(argument, str(args, "style-version"));
|
|
3679
|
+
if (!style) throw new CliError("NOT_FOUND", "This media style or version is not bundled with this CLI.", "Check the id/version or update EditorBrain; do not substitute another version silently.");
|
|
3680
|
+
output(style);
|
|
3681
|
+
}
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
2811
3684
|
if (command === "media" || command === "production") {
|
|
2812
3685
|
const required = (name) => {
|
|
2813
3686
|
const value = str(args, name);
|
|
@@ -3049,7 +3922,7 @@ async function main() {
|
|
|
3049
3922
|
}
|
|
3050
3923
|
if (subcommand === "upload" && argument) {
|
|
3051
3924
|
const target = resolve6(str(args, "dir") ?? process.cwd(), argument);
|
|
3052
|
-
const handle = await
|
|
3925
|
+
const handle = await open8(target, constants8.O_RDONLY | constants8.O_NOFOLLOW);
|
|
3053
3926
|
let bytes;
|
|
3054
3927
|
try {
|
|
3055
3928
|
const info = await handle.stat();
|
|
@@ -3100,13 +3973,13 @@ async function main() {
|
|
|
3100
3973
|
}
|
|
3101
3974
|
if (command === "workflow" && subcommand === "check") {
|
|
3102
3975
|
const client = new Client(await config(args));
|
|
3103
|
-
output(await client.request("/v1/workflow/check", { method: "POST", body: { projectId: str(args, "project"), previousVersion: str(args, "previous-version") } }));
|
|
3976
|
+
output(await client.request("/v1/workflow/check", { method: "POST", body: { projectId: str(args, "project"), previousVersion: str(args, "previous-version"), previousContextVersion: str(args, "previous-context-version") } }));
|
|
3104
3977
|
return;
|
|
3105
3978
|
}
|
|
3106
3979
|
if (command === "search" && subcommand === "brief") {
|
|
3107
3980
|
const path = str(args, "file");
|
|
3108
3981
|
if (!path) throw new Error("Provide --file with the editorial brief JSON after inspecting the actual material.");
|
|
3109
|
-
const handle = await
|
|
3982
|
+
const handle = await open8(resolve6(path), constants8.O_RDONLY | constants8.O_NOFOLLOW);
|
|
3110
3983
|
let brief;
|
|
3111
3984
|
try {
|
|
3112
3985
|
const stat = await handle.stat();
|
|
@@ -3128,7 +4001,7 @@ async function main() {
|
|
|
3128
4001
|
const scriptPath = str(args, "script"), videoPath = str(args, "video");
|
|
3129
4002
|
let script, assetId = str(args, "asset");
|
|
3130
4003
|
const readInput = async (path, max) => {
|
|
3131
|
-
const handle = await
|
|
4004
|
+
const handle = await open8(resolve6(path), constants8.O_RDONLY | constants8.O_NOFOLLOW);
|
|
3132
4005
|
try {
|
|
3133
4006
|
const stat = await handle.stat();
|
|
3134
4007
|
if (!stat.isFile() || stat.size < 1 || stat.size > max) throw new Error(`Input must be a regular file of at most ${max} bytes.`);
|