Skip to main content

ai-mark-lint

EasyxLab tool

Checks, in CI or locally, that the synthetic images, video and audio you generate or publish still carry the machine-readable AI marks the law asks for — and cites the article for every finding.

It reads the three marks that exist in practice — a C2PA manifest (validated with c2pa-python, the Content Authenticity Initiative's open-source C2PA library), IPTC DigitalSourceType in XMP, and the AIGC implicit label of China's GB 45438-2025 — and checks them against:

  • EU AI Act, art. 50(2) — applicable since 2 August 2026 (2 December 2026 for systems already on the market, per the Digital Omnibus, Regulation (EU) 2026/1744), and the Commission's Code of Practice on Transparency of AI-Generated Content.
  • California AI Transparency Act (SB 942 as amended by AB 853 and SB 1000, Bus. & Prof. Code §§ 22757–22757.6) — operative since 2 August 2026, with the latent disclosure rewritten by SB 1000 on 30 September 2026; duties of large online platforms (§ 22757.3.1, as amended by AB 2713) from 1 January 2027.
  • China: the CAC Measures for Labeling AI-Generated Synthetic Content and GB 45438-2025, in force since 1 September 2025, with the TC260 practice guides that fix where the label goes in each file format.

It also has a compare mode: --before A --after B checks a file before and after your pipeline (resize, CDN, CMS, re-encode) and tells you which marks were lost or broken on the way.

$ ai-mark-lint --before generated.jpg --after edited.jpg

ai-mark-lint · 2 file(s) · jurisdictions: eu,ca · before/after mode

ERROR        DIFF-002 [edited.jpg]: The pipeline invalidated the C2PA manifest
             Before: valid. After: invalid (assertion.dataHash.mismatch).
             The step kept the manifest but changed the content: re-sign with the
             original as an ingredient, or do not touch the bytes.
             citation: Regulation (EU) 2024/1689 (AI Act), Art. 50(2); Code of Practice
             on Transparency of AI-Generated Content, Measure 1.2; Cal. Bus. & Prof. Code
             § 22757.3.1(c), as amended by AB 2713 (operative January 1, 2027)

1 error · 0 warnings · 0 undetermined
Zero errors means these rules found no breach in the metadata; it does not certify
compliance, and invisible watermarks were not checked.

(That pair is real: a file signed with the c2patool test certificate, then one field edited with exiftool. The manifest is still in the file and its signature still verifies, but the content hash it signs no longer matches the file.)

Who it is for

  • Providers of generative systems that must mark their outputs: put a sample of your system's outputs through it on every change, so a refactor that drops the digitalSourceType or invalidates the manifest shows up in the pull request.
  • Anyone who publishes synthetic media through a pipeline (CMS, image CDN, transcoder): run the before/after mode on each step. The EasyxLab S3 study measured that no re-save, resize or format conversion it tested kept an embedded C2PA manifest, "keep metadata" options included (0/124 outputs of Pillow, sharp (also with the call chain of the next/image optimizer), ImageMagick and ffmpeg), and that editing one field with exiftool after signing leaves the manifest in the file but invalid: its content hash no longer matches (6/6) — a check that only asks "is there a manifest?" passes it.

What it is, and what it is not

It reads what a file declares about itself. It does not tell whether content is synthetic, and it is meant to be run on files your system generated or manipulated with AI. On a camera photo, its findings make no sense.

It does not see invisible watermarks. Art. 50(2) does not mandate a format, and the Code of Practice asks for two layers: signed metadata and an imperceptible watermark. Watermarks are proprietary and only their vendor can detect them. That is why a file with no metadata marks is a warning for the EU and California, not an error: it may be compliant through a watermark this tool cannot see. Only in China, where the rules say the label goes in the file metadata, is its absence an error.

It does not cover deepfake disclosure (art. 50(4)), visible labels (China's explicit labels; California has required none since SB 1000) or text. Of California's latent disclosure it does not check item (E), whether the GenAI system created or altered the content, nor (F), which only applies from 1 January 2029.

A clean result does not certify compliance. It means these rules found no breach in the metadata of these files. Whether you are a "covered provider" (in California, anyone whose GenAI system is publicly accessible in the state: SB 1000 removed the 1,000,000-users threshold), a signatory of the Code of Practice, or a service provided in China is not something a file can tell.

It never goes to the network. A file may point to a remote manifest; following that link would make a CI linter an HTTP client steered by the file it audits. A remote reference is reported as "reference only" (C2PA-008), never as a valid mark.

Install

pip install ai-mark-lint

The wheel is also attached to each GitHub release, or install from source with pip install git+https://github.com/easybytehub/ai-mark-lint.

Python 3.11+. One runtime dependency, on purpose: c2pa-python, the Python binding of c2pa-rs, the Content Authenticity Initiative's open-source C2PA library. Validating a manifest means verifying a COSE signature, an X.509 chain and hashes over the file; reimplementing that would be inventing cryptography in a tool whose whole value is being believable. Everything else — XMP, EXIF, PNG/RIFF chunks, ID3, MP4 boxes, HEIF — is parsed with the standard library. If c2pa-python cannot load on your platform, the tool keeps working and reports C2PA as undetermined (C2PA-007).

Use

ai-mark-lint outputs/*.jpg outputs/*.mp4                # EU + California (default)
ai-mark-lint outputs/* --jurisdiction eu,ca,cn          # add China
ai-mark-lint outputs/* --jurisdiction none              # only technical C2PA/IPTC checks
ai-mark-lint outputs/* --format json                    # or sarif
ai-mark-lint outputs/* --strict                         # warnings fail too
ai-mark-lint outputs/* --trust-anchors trust.pem        # evaluate the C2PA signer
ai-mark-lint --before original.png --after cdn.webp     # what did the pipeline lose?

Formats, detected by their bytes and not by extension: JPEG, PNG, WebP, AVIF/HEIF, MP4/MOV, MP3, WAV.

Exit codes: 0 no errors · 1 errors (or warnings with --strict) · 2 the tool could not do its job (a file it could not read, no supported format, invalid trust anchors, bad arguments, an internal error). A missing file does not stop the others from being checked: the report covers them and the exit code is still 2. Warnings and undetermined findings do not fail by default: a tool that breaks the build over what it could not determine gets switched off within a week.

China (cn) is opt-in because it is the only jurisdiction where a missing label is an error, and it only binds services provided in China.

In CI (GitHub Actions)

With SARIF output the findings show up in the repository's Security tab:

name: ai-marks
on: [push, pull_request]
permissions:
  contents: read
jobs:
  marks:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
        with:
          persist-credentials: false
      - run: python generate_samples.py --out samples/   # your system's real outputs
      - uses: easybytehub/ai-mark-lint@ad434de28e1559df7e1f652f8951711f5e2b52e5  # v0.1.2
        with:
          files: |
            samples/*.jpg
            samples/*.mp4
          jurisdiction: "eu,ca"
          fail: "false"            # let the SARIF upload run, then fail on your terms
      - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2  # v4.38.2
        if: always()
        with:
          sarif_file: ai-mark-lint.sarif

The action is composite and installs ai-mark-lint from its own code (github.action_path), not from PyPI: the commit you pin in uses: is exactly what runs. Pin it by SHA, as above. files takes one path or glob per line; names with spaces are fine. When the tool exits with 2 (nothing readable, bad trust anchors) the action still leaves a minimal valid SARIF, so upload-sarif with if: always() does not fail on its own. Note that actions/setup-python inside the action changes the job's Python for the steps that follow; set python-version accordingly.

What it checks

Full list, with the literal sentence of each rule, its URL, the date it was read and how it is checked: SPEC.md. In short:

Family Rules
C2PA manifest unreadable (error) · fails validation, e.g. content changed after signing (error) · signer, timestamp or identity credential not trusted (undetermined) · CAWG identity assertion fails [unverified] · first action not c2pa.created/c2pa.opened (error) · c2pa.created without digitalSourceType (error) · unknown source type · remote reference only
IPTC / XMP DigitalSourceType not a vocabulary URI · unreadable XMP · C2PA and IPTC contradict each other
EU no machine-readable AI mark in metadata · AI mark only in unsigned metadata · signed manifest without a timestamp (Code of Practice 1.1.1)
California no latent disclosure · missing provider name, system name and version, creation time or unique identifier (§ 22757.3(a)(1)(A)–(D)). Not checked: (E) whether the system created or altered the content; (F) applies from 1 January 2029
China no AIGC label where the TC260 guides put it · not JSON · missing Label/ContentProducer/ProduceID [unverified] · unknown Label value [unverified] · conflicting labels
Before/after C2PA removed · C2PA invalidated · IPTC AI type removed (error) or changed (warning) · AIGC removed · AIGC producer data rewritten · new manifest no longer declares AI

Requirements that could not be read in the primary source are marked [unverified] and are never errors. Today there are three: the meaning of the Label values and which AIGC key carries which element of Art. 5 (both defined in GB 45438-2025 Annex E, whose text is only served as images), and the CAWG identity assertion codes (C2PA-009), whose specification was not read.

Limitations (v0.1)

  • No watermark detection, no soft-binding lookup, no remote manifests.
  • HEIF/AVIF XMP is found by scanning the raw bytes, not by resolving iloc.
  • JPEG ExtendedXMP and unsynchronised ID3 tags are flagged, not read.
  • No GIF, TIFF, FLV, MKV/WebM or text, although the TC260 guides cover them.
  • Trust lists are yours to provide (--trust-anchors); without one, every real signer is "untrusted" (an undetermined finding, not an error).

Development

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest
.venv/bin/python -m ruff check .
.venv/bin/python -m mypy
.venv/bin/python scripts/generar-fixtures.py     # regenerate the ffmpeg-made media

Fixtures are documented in tests/fixtures/README.md. No private key is committed: each test session creates a throwaway test CA in a temporary directory, signs the fixtures there and deletes it, and a test fails if a key ever lands in the tree. The before/after pairs in tests/fixtures/s3/ come unmodified from the EasyxLab S3 study and were produced by real tools (sharp, ImageMagick, Pillow, ffmpeg, exiftool) and by sharp with the call chain of the next/image optimizer.

A rule is a function Marcas -> list[Hallazgo]; its ID, severity and citation live once, in src/ai_mark_lint/catalogo.py, and a test keeps SPEC.md in sync. A new rule needs the literal citation, a justified severity and a test that fires it and one that does not. When a rule and the law disagree, the law is right and the rule is a bug.

License

Apache-2.0.

Maintained by

EasyxLab, the research lab of EasyByte Hub S. Coop. Mad., a software cooperative in Spain.


EasyxLab · a research lab by EasyByte

Metadata

Release files for ai-mark-lint 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ai-mark-lint 0.1.3
File Size Uploaded Provenance Actions
ai_mark_lint-0.1.3.tar.gz 1.3 MB With provenance Details for ai_mark_lint-0.1.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ai-mark-lint 0.1.3
File Interpreter ABI Platform Actions
ai_mark_lint-0.1.3-py3-none-any.whl Python 3 none any Details for ai_mark_lint-0.1.3-py3-none-any.whl

Total release size: 1.4 MB

Release files / ai_mark_lint-0.1.3.tar.gz

Download URL ai_mark_lint-0.1.3.tar.gz
Size 1.3 MB
Tags Source
SHA-256 checksum
How to use checksums
b0beb1d7424d2835829da9f177e2e77f0f9ec735d2ce39dbb0459f33a78597df
BLAKE2b-256 checksum
How to use checksums
2eea5b26380db784a21d4a46444017401c4be23d3fc13af0a884ddcb0cdffaab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Release files / ai_mark_lint-0.1.3-py3-none-any.whl

Download URL ai_mark_lint-0.1.3-py3-none-any.whl
Size 43.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aeabbf20ddf8c250d308e6c24e302088cb8cdf00bafe716a35c5472d7cdfe1c9
BLAKE2b-256 checksum
How to use checksums
4302b8d05a629361eba796e8f58e3a772b2527b329ed2207ff5cb42d415130c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Security for ai-mark-lint 0.1.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ai_mark_lint-0.1.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page