Reglamento IA Completo - Aviso de imágenes y contenido generados por IA
Reglamento IA Completo: aviso de imágenes y contenido generados por IA (Reglamento Europeo de IA, art. 50). Plugin de WordPress de Piensaenweb.
by Piensaenweb · github.com/claudiopiensaenweb/piensaenweb-ia-completo · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/claudiopiensaenweb/piensaenweb-ia-completo/archive/refs/heads/main.zipBifurcación de Reglamento IA Pro (1.2.0) que añade el aviso de contenido de texto (páginas, entradas, CPT, productos y taxonomías) sobre lo ya existente de imágenes. "Reglamento IA" y "Reglamento IA Pro" se mantienen aparte, sin este añadido, como versiones estables de referencia.
El aviso de contenido se coloca con un único shortcode universal,
[reglia_contenido], en la plantilla del constructor visual — sin parámetros: detecta él solo qué página/entrada/producto/taxonomía se está mostrando. Ajustes propios en Ajustes → Reglamento IA · Contenido.
Plugin de WordPress que recorre las imágenes de cada página, detecta en sus
metadatos si fueron creadas con IA y añade automáticamente un aviso —por
defecto un distintivo superpuesto con icono en una esquina, o alternativamente
un pie de imagen (<figcaption>)— que lo advierte. Orientado a cumplir las
obligaciones de transparencia del Reglamento Europeo de IA (AI Act, art. 50).
Cada adjunto tiene, además, un estado de clasificación persistente con tres valores (No generada por IA / Generada por IA / Modificada por IA) que sobrevive a reexportaciones en Photoshop, redimensionados o conversión a WebP — casos en los que los metadatos originales se pierden pero el aviso debe seguir mostrándose.
Compatibilidad con WPML
- Imágenes: normalmente WPML no duplica el adjunto de medios entre
idiomas (se reutiliza el mismo fichero/ID en todas las traducciones), así
que la clasificación (
_reglia_status) y el marcado de metadatos valen para todos los idiomas a la vez. El aviso en frontend se calcula sobre el HTML final ya renderizado, así que le da igual qué idioma haya resuelto WPML. - Contenido: WPML sí duplica cada página/entrada/producto/término como
un objeto propio por idioma (con su propio ID), así que la clasificación
(
_reglia_content_status) se marca por separado en cada idioma -como cualquier otro campo de esa pantalla de edición-. El shortcode[reglia_contenido]no necesita ningún ajuste: usaget_queried_object(), que WPML ya resuelve a la versión traducida antes de que el shortcode se ejecute. - Textos configurables:
caption_text/caption_text_manipulated(imágenes) ynotice_text_generated/notice_text_reviewed(contenido) se registran mediantewpml_register_single_string(accióninit, enSettings::register_strings()/Content_Settings::register_strings()) bajo el contextoREGLIACOMPLETO_WPML_CONTEXT("reglamento-ia-completo"), y se leen en el frontend a través detranslated( $key ). Sin WPML (o sin su módulo de Traducción de cadenas activo), ambas acciones/filtros no tienen oyentes y no hacen nada:translated()devuelve el mismo valor queget(). - Traducción de fábrica (
Reglamento_IA_Completo_I18n_Defaults): estos 4 textos incluyen ya una traducción a inglés, francés, alemán, italiano, portugués y catalán (castellano,REGLIACOMPLETO_ORIGINAL_LANG, es el idioma en el que están escritos los textos deself::defaults()).translated( $key )decide en este orden:- Si el texto en Ajustes se ha personalizado (ya no es
self::defaults()[$key]), no se aplica ninguna traducción de fábrica -no sabríamos traducir un texto propio- y se usa tal cual en cualquier idioma. - Si no se ha personalizado y el idioma actual (
wpml_current_language) es distinto deREGLIACOMPLETO_ORIGINAL_LANG(español) y está entre los 7 cubiertos, se usa la traducción de fábrica (I18n_Defaults::get( $idioma, $key ); normaliza variantes con región comopt-br/pt-pta las 2 primeras letras). - El resultado de los dos puntos anteriores se pasa como valor por
defecto al filtro
wpml_translate_single_string: si el sitio tiene una traducción manual guardada en WPML → Traducción de cadenas para ese idioma, esa traducción manual gana siempre sobre la de fábrica.
- Sin WPML activo, el filtro
wpml_current_languageno tiene oyentes y devuelvenull: se salta directamente al valor guardado en Ajustes, sin tocar nada. - Importante: la comparación es contra el idioma en el que está escrito el texto de fábrica (español), no contra el idioma principal que tenga configurado el sitio en WPML. Esto es deliberado: si el idioma principal del sitio es, por ejemplo, inglés, y el admin no ha tocado el campo (sigue con el español de fábrica), sus propios visitantes en inglés reciben igualmente la traducción de fábrica al inglés -no hace falta editar nada a mano-, exactamente igual que los visitantes en cualquiera de los otros 6 idiomas cubiertos. Si el idioma principal del sitio NO es ninguno de los 7 cubiertos (p. ej. neerlandés), sus visitantes verían el texto en español sin traducir hasta que el admin lo personalice en Ajustes o lo traduzca a mano en WPML -y en cuanto se personalice el campo, se desactiva la traducción de fábrica para todos los idiomas, no solo para ese-.
- Si el texto en Ajustes se ha personalizado (ya no es
Cómo funciona
template_redirect (prioridad 9999: se anida DENTRO de cualquier
plugin de caché de página completa)
│
▼
ob_start( Renderer.process_buffer ) ──► captura el HTML final
│ de la página COMPLETA
▼ (no solo the_content: así
Renderer.process_document (DOMDocument) también cubre imágenes de
│ page builders como Bricks,
│ por cada <img> Elementor, Divi...)
▼
Engine.resolve_attachment_id() ─► ID del adjunto (solo biblioteca local)
│
▼
Status.get_status() ─► estado PERSISTENTE (_reglia_status)
│ (nunca evaluado, o none | generated | manipulated
│ fichero cambió y + origen: auto | manual
│ origen sigue "auto")
▼
Status.auto_classify()
│
▼
Engine.get_detection() ─► caché en post meta (_reglia_detection)
│ (miss) invalidada por tamaño+mtime del fichero
▼
Detector.detect(fichero ORIGINAL)
│
▼
¿generated/manipulated? ─► envuelve la <img> en <figure><figcaption>Aviso</figcaption>
Puntos clave de diseño:
- Se captura la página HTML completa, no solo
the_content: se usa un buffer de salida (ob_start()) enganchado entemplate_redirect, así que también se detectan imágenes insertadas por page builders (Bricks, Elementor, Divi...) que generan su propio HTML sin pasar por los filtros de contenido de WordPress. - Prioridad de enganche muy alta (9999): si el sitio usa un plugin de caché de página completa (WP Rocket, LiteSpeed Cache, WP Super Cache...), nuestro buffer se abre DESPUÉS del suyo y por tanto queda anidado dentro: el aviso se añade primero, y es ese HTML ya con el aviso el que el plugin de caché guarda en disco. En las visitas servidas desde caché no vuelve a ejecutarse nada de este proceso.
- Sortea el lazy-load nativo de Bricks Builder (y de librerías similares):
validado contra sitios reales de Bricks, su
<img>trae siempre un SVG de relleno vacío ensrcy la URL real endata-src(sin la clasewp-image-Nde Gutenberg).Renderer::real_image_url()pruebadata-src/data-lazy-src/data-originalantes de caer ensrc. - Se analiza el fichero original, no las miniaturas: WordPress suele eliminar los metadatos al redimensionar, así que leerlos de la versión escalada daría falsos negativos.
- Caché por adjunto: la detección se guarda en
_reglia_detectiony solo se recalcula si cambia el fichero. En cada render el coste extra es solo el recorrido del DOM de la página, no volver a leer/analizar el fichero. - Metadatos, no píxeles: las palabras clave se buscan únicamente en las regiones de metadatos (XMP, EXIF, chunks PNG, región C2PA), nunca sobre los datos de imagen, para evitar falsos positivos.
- Respeta el marcado existente: si la
<img>ya está dentro de una<figure>(bloque de Gutenberg), el aviso se añade a la leyenda en vez de duplicar la estructura.
Estado persistente y regla de precedencia
El aviso en el frontend ya no depende de la detección en vivo (que se puede
perder si el fichero se reexporta), sino de un estado guardado en el propio
adjunto (_reglia_status): none / generated / manipulated, con un
origen (auto | manual).
Regla de precedencia (Reglamento_IA_Status::auto_classify()):
- Si el origen es manual, el clasificador automático nunca lo vuelve a tocar — ni para subirlo ni para bajarlo.
- Si el origen es auto, un re-escaneo (disparado cuando cambia la firma
tamaño+mtime del fichero) solo puede subir de
noneagenerated, nunca degradar un valor ya positivo de vuelta anone. Así, si una imagen se reexporta en Photoshop y pierde los metadatos que la delataban, el aviso no desaparece. manipulated("Modificada por IA" en la UI; foto real retocada con IA) es, en la práctica, casi siempre una clasificación manual: los metadatos rara vez distinguen "generado por completo" de "retocado con generative fill".
La evaluación es perezosa: Status::get_status() clasifica un adjunto la
primera vez que se pide — al mostrarlo en el frontend, al abrir su ficha, al
insertarlo en el editor, o al listarlo en la pantalla de revisión masiva—,
por lo que también cubre imágenes subidas antes de instalar el plugin.
Forma del aviso: distintivo (por defecto) o pie de imagen
Configurable en Ajustes → Reglamento IA (display_mode: badge | caption).
badge(por defecto): envuelve la<img>en un<span class="reglia-badge-wrap">con posicionamiento relativo y superpone el aviso en la esquina superior izquierda. El envoltorio hereda las clases CSS originales de la imagen (Renderer::wrapper_class()) — necesario porque muchos temas/page builders dimensionan la imagen mediante sus propias clases (width/height: 100%relativo al padre); sin heredarlas, el envoltorio quedaría sin tamaño y todo colapsaría visualmente. La esquina es arriba-izquierda, no abajo-derecha, porque cuando un tema aplicaobject-fit: containa la imagen (para limitarla a una altura máxima), el hueco que deja ese ajuste suele quedar abajo/derecha — así el aviso no queda fuera de los píxeles visibles.caption: el<figcaption>clásico bajo la imagen (comportamiento original del plugin).
Iconos del distintivo
En modo badge, en vez de texto se muestra un icono SVG (16px de alto) si
el fichero correspondiente existe en assets/icons/:
ai-generated.svg— para el estado Generada por IA.ai-modified.svg— para el estado Modificada por IA.
Si el fichero no existe todavía, Renderer::apply_badge() se repliega
automáticamente al texto configurado (caption_text/caption_text_manipulated)
en vez de dejar el distintivo vacío — no hace falta ningún paso adicional al
añadir los SVG, basta con colocarlos en esa carpeta con esos nombres exactos.
El icono lleva siempre alt con el texto del aviso, para accesibilidad.
Dónde se edita
- Ficha del adjunto / modal de inserción de medios: un desplegable de 3
estados vía
attachment_fields_to_edit/attachment_fields_to_save— aparece automáticamente tanto al editar una imagen ya subida como al insertar una nueva en el editor, sin pantallas a medida. - Medios → Revisión IA: pantalla de revisión masiva, para poner al día de golpe imágenes que ya existían antes de instalar el plugin.
Seguridad
- CSRF: la pantalla de revisión masiva usa
wp_nonce_field()+check_admin_referer(). El campo de la ficha del adjunto reutiliza el nonce (update-post) que WordPress ya exige en su propio flujo de guardado de adjuntos. - Capacidades:
current_user_can('upload_files')para acceder a la pantalla de revisión y ejecutar la acción en lote;current_user_can( 'edit_post', $id)por cada adjunto individual antes de escribir nada (respeta permisos por objeto en sitios multi-autor, no solo la capacidad general) — tanto en el guardado individual como en cada fila de la acción en lote. - Lista blanca, nunca texto libre:
Reglamento_IA_Status::set_manual()rechaza cualquier valor que no sea uno de los 3 estados válidos, sin escribir nada. Los IDs de adjunto de la acción en lote se sanean conabsint()y se verifica que correspondan a un adjunto real (get_post_type() === 'attachment') antes de tocarlos. - Sin SQL propio: toda lectura/escritura usa
get_post_meta()/update_post_meta(); el único$wpdbdel plugin es eldelete()con array (parametrizado) deuninstall.php. - Escapado de salida:
esc_html()/esc_attr()/esc_url()en toda la UI de administración (títulos, nombres de fichero, opciones de los desplegables). - Patrón Post/Redirect/Get: la acción en lote redirige con
wp_safe_redirect()tras procesar, evitando reenvíos accidentales del formulario. - Todos los ficheros PHP comprueban
defined('ABSPATH')para bloquear el acceso directo.
Señales de detección
| Señal | Ejemplo | Confianza |
|---|---|---|
IPTC/XMP DigitalSourceType |
trainedAlgorithmicMedia |
Alta |
| C2PA / Content Credentials | Manifiesto de OpenAI / Firefly | Alta |
| Firma de herramienta | openai, midjourney, firefly, stable diffusion… |
Alta |
| Parámetros PNG (clave) | chunk parameters / prompt / workflow |
Alta |
| Parámetros PNG (patrón) | Steps: … Sampler: … |
Media |
El nivel de confianza mínimo para mostrar el aviso es configurable (Normal = alta+media, Estricto = solo alta).
Estructura
piensaenweb-ia-im-check.php Cabecera del plugin y arranque
includes/
class-reglamento-ia-detector.php Análisis de metadatos (sin dependencias de WP)
class-reglamento-ia-engine.php Resolución de adjunto + caché de detección + firma de fichero
class-reglamento-ia-status.php Estado persistente de 3 valores + regla de precedencia auto/manual
class-reglamento-ia-renderer.php Buffer de página completa y manipulación del DOM
class-reglamento-ia-settings.php Página de ajustes (Ajustes -> Reglamento IA)
class-reglamento-ia-admin.php Columna "IA" + campo de 3 estados en la ficha del adjunto
class-reglamento-ia-bulk-review.php Pantalla de revisión masiva (Medios -> Revisión IA)
assets/css/reglamento-ia.css Estilo del pie de imagen y del distintivo
assets/icons/ai-generated.svg Icono del distintivo para "Generada por IA"
assets/icons/ai-modified.svg Icono del distintivo para "Modificada por IA"
uninstall.php Limpieza al desinstalar
readme.txt Ficha estándar de WordPress.org
Instalación (desarrollo)
- Copia la carpeta a
wp-content/plugins/piensaenweb-ia-im-check. - Actívalo en Plugins.
- Configúralo en Ajustes → Reglamento IA.
Probar la detección
Sube a la biblioteca imágenes con metadatos de IA, por ejemplo:
- Una imagen descargada de ChatGPT/DALL·E o Adobe Firefly (traen C2PA).
- Un PNG exportado desde Automatic1111 o ComfyUI (traen el chunk
parameters/prompt).
En la Biblioteca de Medios (vista de lista) aparecerá la columna IA marcando cada una. Insértalas en una entrada y verás el pie de imagen en el front-end. Desde Medios → Revisión IA puedes revisar y corregir en lote el estado de todas las imágenes ya existentes.
Limitaciones
- Detección por metadatos: si una imagen no los conserva (reexportada, capturas de pantalla, metadatos eliminados), no hay señal que leer.
- No detecta marcas de agua en píxeles (p. ej. SynthID de Google) porque no son metadatos.
- Solo imágenes de la biblioteca local.
Extender
- Nuevas herramientas: añade patrones a
$ai_toolsen el detector. - Excluir ciertas páginas del procesado: añade condiciones a
Renderer::maybe_start_buffer()(junto a la comprobación deis_feed()). - Pre-escaneo al subir: llama a
Engine::get_detection()desde el hookwp_generate_attachment_metadatapara calentar la caché en la subida.
Actualizaciones y publicación de versiones
El plugin se actualiza desde este repositorio de GitHub como cualquier plugin del directorio oficial: la versión nueva aparece en Escritorio → Actualizaciones y en Plugins, y se instala con «Actualizar ahora». En la fila del plugin hay además un enlace Buscar actualizaciones para comprobarlo sin esperar a la caché (6 h).
includes/class-reglamento-ia-completo-actualizador.php consulta la última
release pública del repositorio, compara su etiqueta con REGLIACOMPLETO_VERSION
y descarga el ZIP adjunto. No usa tokens: el repositorio es público.
Para publicar una versión nueva:
- Sube el número de versión en los tres sitios (deben coincidir):
- cabecera
Version:depiensaenweb-ia-completo.php, - constante
REGLIACOMPLETO_VERSION, Stable tag:dereadme.txt(y añade la entrada al Changelog).
- cabecera
- Haz commit y push a
main. - Crea y sube la etiqueta:
git tag v1.0.2 && git push origin v1.0.2.
El workflow .github/workflows/release.yml comprueba que las versiones
coinciden, valida la sintaxis PHP, construye piensaenweb-ia-completo.zip (con
una única carpeta piensaenweb-ia-completo/ en la raíz, sin archivos de
desarrollo según .distignore) y publica la release con el ZIP adjunto.
Importante: la carpeta del plugin en los sitios debe llamarse
piensaenweb-ia-completo. Los sitios con la 1.0.0 (sin actualizador) necesitan instalar la 1.0.1 una vez a mano; a partir de ahí se actualizan solos.