WP Manifestindependent plugin directory
manifest / privacy / piensaenweb-ia-completo

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

★ 0stars
0forks

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.zip

Bifurcació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: usa get_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) y notice_text_generated/notice_text_reviewed (contenido) se registran mediante wpml_register_single_string (acción init, en Settings::register_strings()/Content_Settings::register_strings()) bajo el contexto REGLIACOMPLETO_WPML_CONTEXT ("reglamento-ia-completo"), y se leen en el frontend a través de translated( $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 que get().
  • 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 de self::defaults()). translated( $key ) decide en este orden:
    1. 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.
    2. Si no se ha personalizado y el idioma actual (wpml_current_language) es distinto de REGLIACOMPLETO_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 como pt-br/pt-pt a las 2 primeras letras).
    3. 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_language no tiene oyentes y devuelve null: 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-.

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 en template_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 en src y la URL real en data-src (sin la clase wp-image-N de Gutenberg). Renderer::real_image_url() prueba data-src/data-lazy-src/data-original antes de caer en src.
  • 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_detection y 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 none a generated, nunca degradar un valor ya positivo de vuelta a none. 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 aplica object-fit: contain a 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 con absint() 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 $wpdb del plugin es el delete() con array (parametrizado) de uninstall.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)

  1. Copia la carpeta a wp-content/plugins/piensaenweb-ia-im-check.
  2. Actívalo en Plugins.
  3. 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_tools en el detector.
  • Excluir ciertas páginas del procesado: añade condiciones a Renderer::maybe_start_buffer() (junto a la comprobación de is_feed()).
  • Pre-escaneo al subir: llama a Engine::get_detection() desde el hook wp_generate_attachment_metadata para 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:

  1. Sube el número de versión en los tres sitios (deben coincidir):
    • cabecera Version: de piensaenweb-ia-completo.php,
    • constante REGLIACOMPLETO_VERSION,
    • Stable tag: de readme.txt (y añade la entrada al Changelog).
  2. Haz commit y push a main.
  3. 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.