4viewture / ai-labeling
Transparency fields and frontend labels for AI-generated or AI-modified content (Art. 50 EU AI Act): editor guide, automatic detection from file metadata (IPTC/XMP, C2PA, EXIF, PNG text chunks), EU icons in content, images, video and audio (WCAG 2.2 AA).
Package info
github.com/4viewture/ai_labeling
Type:typo3-cms-extension
pkg:composer/4viewture/ai-labeling
Requires
- php: ^8.1
- typo3/cms-backend: ^12.4 || ^13.4 || ^14.0
- typo3/cms-core: ^12.4 || ^13.4 || ^14.0
- typo3/cms-extbase: ^12.4 || ^13.4 || ^14.0
- typo3/cms-fluid: ^12.4 || ^13.4 || ^14.0
- typo3/cms-frontend: ^12.4 || ^13.4 || ^14.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0
- typo3/cms-filelist: ^12.4 || ^13.4 || ^14.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
4viewture/ai-labeling (extension key ai_labeling) adds transparency fields for AI-generated or
AI-modified content to file metadata (sys_file_metadata), content elements (tt_content)
and all other eligible record tables (pages, news, …), following the guide
„KI-generierte Inhalte kennzeichnen“
(Mittelstand-Digital Zentrum Berlin). Transparency obligations apply from 2 August 2026.
Supports TYPO3 12.4 LTS, 13.4 LTS and 14, PHP 8.1+.
Editor guide (German): Documentation/Leitfaden.md
Example file in file_metadata
Features
-
Own „KI“ tab on every type of every eligible table – columns, palettes and tab are added in
AfterTcaCompilationEvent(tables/types of extensions loaded later are covered), database columns viaAlterTableDefinitionStatementsEvent(sys_file_metadata/tt_contentalso inext_tables.sql). -
Eligible tables (
tables = *): all tables with types, exceptbe_*,sys_*(butsys_file_metadatais always included),fe_users,fe_groups, cache/index/scheduler/impexp tables,adminOnly,readOnly,is_staticand root-level (rootLevel = 1) tables. Restrict withtables = tt_content,pages,…orexcludeTables. Flush caches and run the database compare after changing it. -
Record icon overlay for all labeled tables (page module, list module, form header, element browser …):
ModifyRecordOverlayIconIdentifierEvent(13+) /overrideIconOverlayhook (12). Core status overlays (hidden, start/endtime, access, deleted) keep priority. The list module only selects visible columns, soModifyDatabaseQueryForRecordListingEventadds the two needed fields. Rows without the fields (e.g. page tree) get no overlay – no extra queries. -
Decision questions from the guide (deepfake criteria, testimonial, product altered, public-interest text, human review) with field descriptions.
-
Live assessment (
<ai-labeling-guide>web component) on top of the tab: required / recommended / optional / none, reasons, resulting label – plus the collapsible guide. -
Label mode: automatic / always / never (warning flash message if a required label is switched off).
-
Computed, persisted fields for the frontend:
tx_ailabeling_status,tx_ailabeling_active,tx_ailabeling_reasons(calculated on save by a DataHandler hook). -
Automatic detection on upload/replace (
AfterFileAddedEvent,AfterFileReplacedEvent):- IPTC Digital Source Type in XMP/C2PA (
trainedAlgorithmicMedia,compositeWithTrainedAlgorithmicMedia,compositeSynthetic,algorithmicallyEnhanced) - C2PA / Content Credentials claim generator
- PNG text chunks of Stable Diffusion WebUI, ComfyUI, InvokeAI, NovelAI
- known AI tool names in metadata only (XMP, EXIF, PNG text, ID3, PDF info, MP4
©too)
- IPTC Digital Source Type in XMP/C2PA (
-
File list indicators
- Icon overlay via
ModifyIconForResourcePropertiesEvent(12.4–14): red AI star = labelled, outlined star = AI generated/modified but not labelled. Shown wherever the core renders file icons (file list, element browser, file references, live search). Core overlays (e.g. missing file) win. - Columns AI labelling status / AI label active selectable in the column selector of the file list (metadata fields, readOnly – not part of the edit form).
- TYPO3 14+: text badge in the name column and
data-ai-labeling*row attributes viaAfterFileListRowPreparedEvent.
- Icon overlay via
-
Rescan existing files (CLI / scheduler), same detection as on upload:
vendor/bin/typo3 ailabeling:scan --dry-run # planned changes per file, nothing written vendor/bin/typo3 ailabeling:scan --folder=1:/user_upload/ # one folder, recursive vendor/bin/typo3 ailabeling:scan --file=123 --file=1:/a.jpg # single files (uid or combined identifier) vendor/bin/typo3 ailabeling:scan --skip-detected # only files never detected before vendor/bin/typo3 ailabeling:scan --storage=1 --limit=500 [--override-manual] [-v]
Output: table of changed files (involvement / mode / label active as
old → new, tool, evidence) and a summary (scanned, found, changed, up to date, manual decisions kept, errors). Editorial decisions are kept: an involvement set by an editor, or changed after an earlier detection (also to "no AI"), is not overwritten without--override-manual– only the evidence is updated. Label changes invalidate processed images and page caches. Roughly 0.3 s per file (contents are read up toscanLimitMb). -
File list context menu "Scan for AI content" (files of type image, audio, video, application): shows the evidence and the planned changes in a modal (like
--dry-run), Apply writes them and reloads the file list. Only for users who may edit the file metadata and the AI fields (tables_modifyonsys_file_metadataand the exclude fieldtx_ailabeling_involvement).
Installation
composer require 4viewture/ai-labeling vendor/bin/typo3 extension:setup
Fields are exclude => true – grant them to editor groups (Allowed excludefields).
Extension configuration
| Setting | Default | Meaning |
|---|---|---|
tables |
* |
Tables with AI fields (* or comma list) |
excludeTables |
– | Tables to skip |
autoDetect |
1 | Analyse files on upload/replace |
autoLabel |
1 | Set label mode of detected files to "always label" |
scanLimitMb |
32 | Bytes read per file (head + tail half each for larger files) |
additionalSignatures |
– | Comma-separated extra tool names |
Fields
| Field | Type | Notes |
|---|---|---|
tx_ailabeling_involvement |
int | 0 none, 1 assisted, 2 modified (AI MODIFIED), 3 generated (AI GENERATED) |
tx_ailabeling_tool |
string | tool / model |
tx_ailabeling_real_reference, _deceptive, _synthetic_person, _product_altered |
bool | deepfake / product questions |
tx_ailabeling_public_interest, _human_review, _reviewer |
bool/string | text questions |
tx_ailabeling_mode |
int | 0 auto, 1 always, 2 never |
tx_ailabeling_label_text |
string | empty = default text (LabelTextResolver) |
tx_ailabeling_detected, _detection |
bool/text | automatic detection + evidence |
tx_ailabeling_status |
int | computed: 0 none, 1 optional, 2 recommended, 3 required |
tx_ailabeling_active |
bool | computed: render a label in the frontend if 1 |
tx_ailabeling_reasons |
csv | computed reason keys |
Frontend output
Rendered only for records/files with tx_ailabeling_active = 1. Label type: modified if
tx_ailabeling_involvement = 2, otherwise generated. Text: tx_ailabeling_label_text of the record,
else the default text in the site language of the request.
Setup
- TYPO3 12.4 – 14: include the static template "AI Labeling: frontend output", or
- TYPO3 13.4+: add the site set
4viewture/ai-labelingto the site (dependencies:inconfig/sites/<site>/config.yamlor the site module). Do not use both.
Constant / site setting (plugin.tx_ailabeling.) |
Default | Meaning |
|---|---|---|
css.include |
1 | include ai-label.css (page.includeCSS) |
content.enable |
1 | label content elements (tt_content.stdWrap.postUserFunc) |
label.text |
visible |
hidden = text only for screen readers (ai-label__text--sr-only), icon visible |
label.symbol |
ai |
ai (EU icon), ki (German variant, not an official EU icon), auto (KI on German site languages) |
label.secondLayer |
0 | disclosure button with explanation (Popover API, ai-label.js keeps aria-expanded in sync) |
images.burnIn |
1 | burn the EU "AI" symbol into processed frontend images |
images.position |
top-right |
top-right, top-left, bottom-right, bottom-left |
images.sizePercent / minSize / maxSize |
8 / 24 / 96 | symbol size: % of the shorter edge, clamped (px) |
images.altText |
1 | append the label text to non-empty alt, add the label to <figcaption> |
images.symbol |
ai |
burned-in symbol ai or ki (no auto: processed files are shared by all languages) |
media.overlay |
1 | label as overlay at the top of videos; 0 = label below the player |
German icon variants ("KI")
Resources/Public/Icons/EU/ki-*.svg ("KI", "KI GENERIERT", "KI VERÄNDERT"; black, white, 50 %) are
generated from the official files by Build/generate-german-icons.py: official letters are reused,
only K, V and Ä come from Rubik (the typeface of the artwork, SIL OFL 1.1) at matching weight, cap
height and tracking. They are not official EU icons – the Code of Practice (Measure 1.1 a) uses
"AI" unless English is incompatible with national language law, hence ai is the default.
Label component
<span class="ai-label ai-label--generated" data-ai-label="generated"> <svg class="ai-label__icon" aria-hidden="true" focusable="false">…</svg> <span class="ai-label__text">KI-generiert</span> </span>
Theming via custom properties on any ancestor (keep text contrast ≥ 4.5:1):
:root { --ai-label-color: #fff; --ai-label-background: #e60005; --ai-label-border-color: #e60005; } /* or: <body class="ai-label-theme-red"> */
Further properties: --ai-label-font-size (0.875em), --ai-label-radius, --ai-label-focus-color,
--ai-label-overlay-offset.
Content elements
tt_content.stdWrap.postUserFunc = FourViewture\AiLabeling\Frontend\ContentElementLabeler->process
(TypoScript of the extension, only if content.enable = 1). It never wraps existing markup:
- skips whitespace, comments, the empty anchors
<a id="c123"></a>and empty spacer elements, - adds
data-ai-label="generated|modified"to the first real element, - inserts the label after the heading inside
<header>(or.frame-headerof bootstrap_package), - otherwise as first child of the first element (
<span class="ai-label-line">, which takes a full row inside flex/grid parents), - without element (or for tables, lists, media, void elements) it prepends
<p class="ai-label-line">.
stdWrap has only one postUserFunc: if your site already uses tt_content.stdWrap.postUserFunc,
set content.enable = 0 and call the labeler from your own function.
Images: burned-in symbol
FAL processor AiLabelImageProcessor ($GLOBALS['TYPO3_CONF_VARS']['SYS']['fal']['processors'], after
SvgImageProcessor/DeferredBackendImageProcessor, before OnlineMediaPreviewProcessor/LocalImageProcessor)
composites the compact EU "AI" symbol (not the "AI GENERATED" text icon – no images of text,
WCAG 1.4.5) onto processed images of labelled files:
Image.CropScaleMask/Image.Preview, not in backend requests (no backend previews), jpg/png/webp/ static gif, and the preview images of YouTube/Vimeo files. SVG and animated GIF/WebP are skipped.- Default top right (Code of Practice, Sub-measure 1.2.2 a), 8 % of the shorter edge, 24–96 px,
margin 2 % (min 4 px). Images with a shorter edge below 3 ×
minSizeare not burned (alt text and caption still label them). The Code sets no numeric sizes (Measure 1.1 c) – adjust via constants. - Black or white variant by measured contrast of the target corner (black against the darkest 5 %, white against the brightest 5 % of 576 samples, WCAG relative luminance); if neither reaches 3:1, the black symbol gets an opaque white backing plate.
- If the core would deliver the original unprocessed (no resize needed), a derived file is created.
- GD, Imagick as fallback. Errors: no symbol, warning in the log. PNGs are generated from the official
SVGs by
Build/rasterize-icons.sh. GD re-encodes the image (EXIF/ICC are not kept). - Invalidation: when
tx_ailabeling_active/involvementof a file change (DataHandler hook forsys_file_metadata,AutoLabeler/ailabeling:scan), all processed files of the original are deleted (files andsys_file_processedfilerows) and the page cache of all pages referencing the file is flushed (sys_file_reference+sys_refindex→ tagspageId_<pid>,<table>_<uid>,tx_news_uid_<uid>/tx_news_pid_<pid>). Label text changes flush the pages only. - Changing
images.*settings does not change the processed file names: clear processed files afterwards (Maintenance → Remove Temporary Assets, orvendor/bin/typo3 cleanup:localprocessedfiles). - Processed files are shared between sites/contexts with the same processing configuration; a backend request never burns the symbol in, so a file processed there with an identical configuration first stays without symbol until it is invalidated.
Images: alt text and captions
Listener on AfterCacheableContentIsGeneratedEvent (once per generated page; content API:
getContent()/setContent() in TYPO3 14, getController()->content in 12.4/13.4):
- every
<img>whosesrc,srcset,data-src,data-srcset(or<source srcset>of its<picture>) points to a labelled file – original or processed – gets(KI-generiert)(label text in the frontend language) appended to a non-emptyalt.alt=""(decorative) stays untouched. - inside
<figure>with<figcaption>the label component is appended to the caption (once per figure). - URL → file: request-scoped registry fed by
AfterFileProcessingEvent(dispatched for new and existing processed files), DB fallback for other URLs (local storages,sys_file_processedfile/sys_file, filtered bytx_ailabeling_active). Only the matched tags are edited (attribute level), no DOM round trip. - Non-cacheable output (
USER_INT) is not rewritten.
Video, audio, online media
AiLabelMediaRenderer (priority 1000, registered like the core renderers via
RendererRegistry::registerRendererClass()) decorates the renderer that would render a labelled file
(core VideoTagRenderer, AudioTagRenderer, YouTubeRenderer, VimeoRenderer or any other
renderer): it outputs the unchanged player HTML followed by the label component.
- Video / YouTube / Vimeo:
ai-label--overlay. Inside@supports (anchor-name: --x) and (anchor-scope: --x)the label is positioned via CSS anchor positioning at the top right of the player (Code of Practice, Sub-measure 1.2.2 a), out of flow,pointer-events: none, never over the control bar (WCAG 2.4.11).anchor-scopebinds each label to its own player. Without support: small static label right after the player. - Audio (and
media.overlay = 0): visible label after the player (ai-label--media). The spoken disclaimer at the beginning of the recording is editorial work (see guide).
Two-click / privacy placeholders (e.g. EXT:video_shariff) render the player HTML only after the
click; the label is part of it, but the preview before the click is not a labelled FAL image. Add the
label to the placeholder in your template override, e.g. in Partials/Media/Type/Video.html of
video_shariff after <f:image … /> inside the play link:
<ai:label record="{file}" variant="overlay" />
(variant="overlay" anchors to the preceding <img>; the label text becomes part of the link name.)
ViewHelpers (news and other records)
Namespace ai is registered globally (http://typo3.org/ns/FourViewture/AiLabeling/ViewHelpers).
<ai:label record="{newsItem}" /> <!-- Extbase model --> <ai:label record="{data}" table="tx_foo" /> <!-- record array --> <ai:label table="tx_foo" uid="{uid}" /> <!-- uid, overlaid into the frontend language --> <ai:label record="{record}" /> <!-- TYPO3 13.4+ Record object --> <ai:label record="{newsItem}" textVisible="0" /> <!-- icon only, text for screen readers --> <ai:label record="{file}" variant="overlay" /> <!-- FAL file / file reference (overlay|media) --> <ai:condition.labeled record="{newsItem}"> <f:then>…</f:then> <f:else>…</f:else> </ai:condition.labeled>
Models do not know the AI fields; they are loaded by uid (_localizedUid for translations), cached
per request.
EXT:news, list view – Resources/Private/Partials/List/Item.html (copy into your site package):
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" xmlns:ai="http://typo3.org/ns/FourViewture/AiLabeling/ViewHelpers" data-namespace-typo3-fluid="true"> … <div class="header"> <h3> <n:link newsItem="{newsItem}" settings="{settings}" title="{newsItem.title}"> <span itemprop="headline">{newsItem.title}</span> </n:link> </h3> <ai:label record="{newsItem}" /> </div>
EXT:news, detail view – Resources/Private/Templates/News/Detail.html, after the headline:
<div class="header"> <h1 itemprop="headline">{newsItem.title}</h1> <ai:label record="{newsItem}" /> </div>
Keep the label outside of links and headings (accessible names stay clean). Images of the news item are labelled automatically (burned-in symbol, alt text, figcaption).
Generic record (FLUIDTEMPLATE / PAGEVIEW) – DataProcessor alias ai-label:
lib.myRecord = FLUIDTEMPLATE
lib.myRecord {
dataProcessing.10 = ai-label
# optional: table = tx_foo (default: current table), as = aiLabel
}
<f:if condition="{aiLabel.active}">{aiLabel.html -> f:format.raw()}</f:if> <!-- also available: {aiLabel.type} (generated|modified), {aiLabel.text} -->
Architecture
Classes/
Configuration/TcaFields.php shared TCA (columns, palettes, tab, SQL)
Configuration/LabelingTables.php which tables get the fields
Detection/AiContentDetector.php TYPO3-independent metadata analysis
Service/LabelEvaluator.php decision logic of the guide (mirrored in label-guide.js)
Service/AutoLabeler.php detector -> FAL metadata
Service/LabelTextResolver.php label text / defaults (backend: $GLOBALS['LANG'], frontend: site language)
Frontend/ LabelRenderer (component), ContentElementLabeler, RecordLabelResolver, settings
Html/ targeted string insertion helpers (no DOM round trip)
ViewHelpers/, DataProcessing/ <ai:label>, <ai:condition.labeled>, dataProcessing ai-label
EventListener/ TCA/SQL for all tables, detection, icon overlays, list module select,
image URL registry + alt/figcaption rewriting
Service/RecordOverlay.php overlay decision for record icons
Hooks/DataHandlerHook.php persist computed status on save, invalidate processed files/pages
Resource/ AiLabelImageProcessor + ImageLabelCompositor (burn-in), AiLabelMediaRenderer
Imaging/Contrast.php WCAG contrast, symbol variant and placement
Service/LabelInvalidator.php processed files + page cache of a file
Form/Element/GuideInformation.php FormEngine fieldWizard: guide + live assessment
Command/ScanCommand.php ailabeling:scan (rescan, --dry-run)
ContextMenu/, Controller/ file list context menu "Scan for AI content" + AJAX endpoint
Compatibility notes (verified against the core sources of 12.4, 13.4 and 14.3; Fluid 2.15 / 5.x):
- No PHP attributes for listeners/commands (v12) – everything in
Configuration/Services.yaml. Exception:#[AsAllowedCallable]onContentElementLabeler::process()– required for TypoScript callables in TYPO3 14 (Breaking #108054); the attribute is never instantiated in 12/13. - No Fluid
StandaloneView(removed in v14), file types as integers instead ofAbstractFile::FILETYPE_*. - FormEngine node without own constructor (v12 constructor / v13+
setData()). AfterCacheableContentIsGeneratedEvent:getContent()/setContent()only in 14, 12.4/13.4 usegetController()->content.LocalImageProcessor::processTaskWithLocalFile(): public in 12/13, protected in 14 (the processor is a subclass).TaskInterface::getTargetFileExtension()only in 13.4+ (12.4:AbstractTask).- Rendering context request: attribute
ServerRequestInterface(13.4/14.3),getRequest()(12.4). - Extbase table of a model:
DataMapFactory::buildDataMap()(12.4–14.3,@internal),DataMapper::getDataMap()fallback. - File renderers via
RendererRegistry::registerRendererClass()(12.4–14.3; TYPO3 15 uses tagged services). - Site set enums as map (
value: label): array in 13.4, object in 14.x – both read.
Limitations
- Detection is heuristic. Metadata is often stripped by editing tools or social networks; invisible watermarks (e.g. SynthID) cannot be read. A tool name inside a human-written caption can cause a false positive – the editor reviews the result in the AI tab.
- Detection writes the default-language metadata record only.
- Tiles view with thumbnails: the thumbnail replaces the icon, so the overlay is only visible for files without preview; the core offers no event for tiles (12–14).
- Record overlay only when the core shows no status overlay (hidden etc. win) and only where the rendered row contains the AI fields (not in the page tree).
Tests and QA
GitHub Actions (.github/workflows/ci.yml) on push, pull requests and weekly:
| Job | Content |
|---|---|
| PHP lint | php -l on PHP 8.1 – 8.5 |
| Composer, XLIFF, YAML, SVG | composer validate, well-formed XLIFF/SVG, de/en trans-unit ids match, yamllint |
| Coding guidelines | php-cs-fixer (PER-CS 2.0 + TYPO3 conventions, Build/php-cs-fixer.php) |
| TYPO3 12.4 / 13.4 / 14 × PHP | PHPStan level 5 (Build/phpstan.neon) and unit tests for each TYPO3 version |
Locally (standalone checkout, dependencies in .Build/):
composer install composer ci # validate, lint, cgl, phpstan, unit tests composer fix:php:cs # apply coding guidelines
Inside a TYPO3 project:
vendor/bin/phpunit -c vendor/4viewture/ai-labeling/phpunit.xml.dist --bootstrap vendor/autoload.php
The unit tests cover the decision logic, the detector (synthetic JPEG/XMP, PNG chunks, C2PA/CBOR,
large files), the label component and settings, the content element insertion rules (fixtures:
fluid_styled_content 12–14, frame_class = none, bootstrap_package, b13/container, empty output),
alt/figcaption rewriting, contrast/placement and GD compositing.
Browser tests (Playwright + axe-core; desktop, 200 % zoom, 320 px, forced colors, text spacing): Tests/Acceptance/README.md.
