RC-5 Versiopn: 1.6.3
All checks were successful
release-tag / release-image (push) Successful in 1m47s

This commit is contained in:
2026-07-22 05:58:38 +02:00
parent 862f86dc3e
commit 133fb10a93
25 changed files with 877 additions and 105 deletions

View File

@@ -1,18 +1,23 @@
# Changelog
## 1.6.3 - 2026-07-21
- Hardened EU AI Act guidance against the Commission Article 50 guidelines published on 20 July 2026.
- Declaration schema 1.2 adds optional Article-50 `legalContext` categories.
- Generator distinguishes voluntary transparency from potentially legally relevant disclosures.
- Deepfake, public-interest text and artistic/creative/satirical/fictional context can be recorded explicitly.
- Article badges use clearer first-exposure wording when the supplied context indicates a likely disclosure case.
- Emoji embeds add adjacent plain text when a likely legal disclosure is indicated.
- Public-interest assessment considers substantive review together with expressly named editorial responsibility.
- Editorial responsibility can be classified as publisher responsibility or another responsible party and is stored explicitly.
- Declaration pages show a cautious, non-binding Article-50 assessment and no longer imply editorial responsibility unless it is expressly stated.
- Presets no longer infer human/editorial review.
- Added first-exposure, machine-readable provider-marking and emoji-only disclosure guardrails.
- Added configurable Impressum, privacy and accessibility pages plus security hardening for logs, proxy trust, CSP, HSTS and metrics.
## Unreleased
- Hardened EU AI Act guidance against the Commission Article 50 guidelines published on 20 July 2026.
- Presets no longer infer human/editorial review; editorial responsibility is explicitly configurable and must include a responsible name when declared.
- Added first-exposure, machine-readable provider-marking and emoji-only disclosure guardrails plus an EU AI Act operator checklist.
- Added an optional square `emoji` SVG theme with dedicated symbols for no-AI, research, summarisation, full generation and article-level declarations.
- Added generator, OpenAPI, documentation and accessibility support for the emoji theme.
- Added configurable Impressum, privacy and accessibility pages with visible placeholder warnings and strict production validation.
- Added secure defaults for client-IP logging, reverse-proxy trust, HSTS, CSP nonces and Prometheus access.
- Added legal/security deployment guidance and operator checklists.
- Added request limits, stricter JSON handling and configuration URL validation.
- Redacted internal licence metadata from the public capabilities endpoint.
- Added regression tests for legal routes and security controls.
- No unreleased changes.
## 1.6.2

View File

@@ -21,4 +21,4 @@ build:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o bin/ai-disclosure ./cmd/server
docker-build:
docker build -t ai-disclosure-standard:1.6.2-local .
docker build -t ai-disclosure-standard:1.6.3-local .

View File

@@ -1,4 +1,4 @@
# AI Disclosure Standard 1.6.2
# AI Disclosure Standard 1.6.3
Ein zustandsloser Go-Dienst für sichtbare und maschinenlesbare Erklärungen zur KI-Nutzung in Artikeln, Webseiten und einzelnen Inhaltsbestandteilen.
@@ -268,3 +268,9 @@ Nicht enthalten sind:
- Widerrufsverwaltung oder Lease-Signierung.
Diese Funktionen werden nur in der eigenständigen Universal License Platform betrieben.
### Art.-50-Kontext (Schema 1.2)
Optional kann `legalContext` den rechtlich relevanten Veröffentlichungskontext als Selbsteinordnung dokumentieren: `deepfake`, `publicInterestText`, `artisticCreativeSatiricalFictional` oder `otherVoluntary`. Der Generator nutzt diese Angaben für eine vorsichtige, nicht bindende Einschätzung und für eine eindeutigere Erstkennzeichnung. Ein Deepfake ist dabei nicht jedes KI-generierte Bild. Die Einordnung muss durch den Publisher anhand des konkreten Inhalts und Veröffentlichungskontexts erfolgen.
Bei `publicInterestText` berücksichtigt die Einschätzung, ob eine substanzielle inhaltliche Prüfung (`editorial` oder `expert`) und eine ausdrücklich benannte `editorialResponsibility` dokumentiert sind. Bei einer als wahrscheinlich kennzeichnungspflichtig eingeordneten Nutzung erzeugt der Badge eine unmittelbar verständliche Beschriftung statt nur „Artikeltransparenz“. Im `emoji`-Modus ergänzt der generierte Einbettungscode bei solchen Fällen zusätzlich sichtbaren Text.

View File

@@ -25,7 +25,7 @@ spec:
type: RuntimeDefault
containers:
- name: app
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.6.2
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.6.3
imagePullPolicy: IfNotPresent
ports:
- name: http

View File

@@ -1,14 +1,14 @@
version: "3.9"
services:
app:
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.6.2
image: ghcr.io/REPLACE_ME/ai-disclosure-standard:1.6.3
environment:
BASE_URL: https://ai.trustednet.eu
BASE_URL: https://ai.example.org
PUBLIC_NAME: AI Usage Disclosure
CONTACT_URL: https://ai.trustednet.eu
SALES_URL: "${SALES_URL:-https://ai.trustednet.eu}"
CONTACT_URL: https://b1tsblog.org/page/ai
SALES_URL: "${SALES_URL:-https://b1tsblog.org/page/ai}"
DEFAULT_LANGUAGE: de
TRUST_PROXY: "true"
TRUST_PROXY: "false"
LOG_CLIENT_IP: "false"
ENABLE_HSTS: "true"
METRICS_ENABLED: "false"

View File

@@ -1,6 +1,6 @@
# Mehrsprachige Hintergrundseite
Version 1.6.2 stellt unter `/background` eine eigenständige Informationsseite zur KI-Kennzeichnung und zu Artikel 50 des EU AI Act bereit.
Version 1.6.3 stellt unter `/background` eine eigenständige Informationsseite zur KI-Kennzeichnung und zu Artikel 50 des EU AI Act bereit.
## Routen

View File

@@ -142,3 +142,16 @@ Wenn ein Inhalt **möglicherweise** unter Artikel 50 Absatz 4 fällt, ist die re
- das JSON-LD nur als zusätzliche Dokumentation behandeln.
Bei Zweifeln über die Ausnahme wegen menschlicher Prüfung ist eine **zusätzliche Offenlegung regelmäßig die risikoärmere technische Entscheidung**, solange dadurch keine anderen rechtlichen Pflichten verletzt werden.
## Strukturierter rechtlicher Kontext in Schema 1.2
Die Deklaration kann optional `legalContext.categories` enthalten. Unterstützt werden `deepfake`, `publicInterestText`, `artisticCreativeSatiricalFictional` und `otherVoluntary`. Diese Angaben sind ausdrücklich als Selbsteinordnung konzipiert und ersetzen keine rechtliche Subsumtion.
Der Generator trennt damit zwei Ebenen:
1. **Erstkennzeichnung am Inhalt:** Bei einer als voraussichtlich offenlegungspflichtig eingeordneten Nutzung wird eine verständliche Beschriftung wie „KI-generierter / manipulierter Inhalt“ oder „KI-generierte / bearbeitete Inhalte“ erzeugt.
2. **Zweite Ebene:** Die `/declaration`-Seite dokumentiert KI-Anteil, Tätigkeiten, menschliche Prüfung, Nachweisgrundlage, rechtlichen Kontext und gegebenenfalls redaktionelle Verantwortung.
Für `publicInterestText` wird eine mögliche Ausnahme nur als Möglichkeit angezeigt, wenn sowohl eine substanzielle Prüfung (`editorial` oder `expert`) als auch eine ausdrücklich benannte redaktionelle Verantwortung dokumentiert sind. Formale Prüfung (`basic`) reicht dafür nicht. Bei `deepfake` bleibt die Einschätzung auf Offenlegung gerichtet; die zusätzliche Kategorie `artisticCreativeSatiricalFictional` weist auf die begrenzte, angemessene Offenlegungsform für evident kreative Werke hin.
Das System formuliert bewusst „spricht vieles dafür“ beziehungsweise „kann in Betracht kommen“ und gibt keine verbindliche Rechtsentscheidung aus.

View File

@@ -0,0 +1,101 @@
# EU AI Act Review Ergänzung vom 21.07.2026
## Ergebnis
Die zusätzlichen Anforderungen wurden in Version 1.6.3 / Declaration Schema 1.2 aufgenommen. Das Projekt trennt nun ausdrücklich zwischen freiwilliger KI-Transparenz und einem vom Nutzer selbst angegebenen rechtlichen Kontext für Artikel 50.
Eine Nutzung des Standards allein begründet weiterhin keine vollständige EU-AI-Act-Konformität. Die konkrete Rolle (Provider/Deployer), das eingesetzte KI-System, der Inhalt, der Veröffentlichungskontext sowie weitere Pflichten des AI Act müssen separat geprüft werden.
## Neu aufgenommen
### 1. Strukturierter rechtlicher Kontext
`legalContext.categories` unterstützt:
- `deepfake`
- `publicInterestText`
- `artisticCreativeSatiricalFictional`
- `otherVoluntary`
Die Angaben sind als Selbsteinordnung konzipiert und nicht als automatische rechtliche Subsumtion.
### 2. Deepfake-Abgrenzung
Der Generator erklärt, dass nicht jedes KI-generierte Bild ein Deepfake ist. Die Kategorie ist für KI-generierte oder manipulierte Bild-, Audio- oder Videoinhalte vorgesehen, die bestehenden oder plausibel existierenden Personen, Objekten, Orten, Entitäten oder Ereignissen ähneln und fälschlich authentisch oder wahr erscheinen können.
### 3. Texte zu Angelegenheiten von öffentlichem Interesse
Für `publicInterestText` berücksichtigt die vorsichtige Einschätzung:
- ob eine substanzielle menschliche Prüfung oder redaktionelle Kontrolle dokumentiert wurde (`editorial` oder `expert`), und
- ob eine redaktionell verantwortliche natürliche oder juristische Person ausdrücklich benannt wurde.
Formale Prüfung (`basic`) wird nicht als ausreichende inhaltliche Prüfung behandelt.
### 4. Redaktionelle Verantwortung
Die Verantwortung wird nun expliziter gespeichert:
```json
{
"editorialResponsibility": {
"assumed": true,
"role": "publisher",
"name": "Beispielredaktion",
"url": "https://example.org/impressum"
}
}
```
Unterstützte Rollen:
- `publisher`
- `other`
Eine bloße URL ohne benannte verantwortliche Stelle wird nicht akzeptiert.
### 5. Zwei Ebenen der Kennzeichnung
Bei freiwilliger Transparenz kann weiterhin z. B. „Artikeltransparenz“ verwendet werden.
Wenn die Selbsteinordnung auf einen voraussichtlich kennzeichnungspflichtigen Artikel-50-Fall hindeutet, wird die Erstkennzeichnung klarer formuliert, z. B.:
- „KI-generierter / manipulierter Inhalt“
- „KI-generierte / bearbeitete Inhalte“
Die ausführliche zweite Ebene bleibt die `/declaration`-Seite.
### 6. Emoji-Modus
Der quadratische Emoji-Modus bleibt verfügbar. Bei einem als wahrscheinlich offenlegungspflichtig eingeordneten Fall ergänzt der generierte HTML-/Markdown-Einbettungscode neben dem Emoji zusätzlich sichtbaren Klartext. Das Emoji wird weiterhin nicht als offizielles EU-Icon oder als alleiniger Konformitätsnachweis dargestellt.
### 7. Custom-Badge-Schutz
In einem vom Nutzer als wahrscheinlich gesetzlich relevanten Artikel-50-Fall kann ein Pro-Custom-Text die eindeutige Erstkennzeichnung nicht mehr durch eine abstrakte Badge-Botschaft ersetzen. Farben und linke Beschriftung bleiben anpassbar; die entscheidende Offenlegungsbotschaft bleibt verständlich.
### 8. Vorsichtige Einschätzung auf der Declaration-Seite
Die Seite zeigt nun eine gesonderte „Vorsichtige Art.-50-Einschätzung“.
Beispiele:
- Deepfake: „spricht vieles dafür“, dass eine klare Offenlegung spätestens bei erster Exposition erforderlich ist.
- Public-Interest-Text ohne dokumentierte Ausnahmevoraussetzungen: „spricht vieles dafür“, dass eine Offenlegung erfolgen sollte.
- Public-Interest-Text mit substantieller Prüfung und ausdrücklich benannter redaktioneller Verantwortung: die Ausnahme „kann in Betracht kommen“.
Es werden bewusst keine verbindlichen Formulierungen wie „rechtlich zwingend“ oder „AI-Act-konform“ ausgegeben.
## Aktuelle offizielle Grundlagen
- Artikel 50 AI Act: https://ai-act-service-desk.ec.europa.eu/en/ai-act/article-50
- Kommissionsleitlinien vom 20.07.2026: https://digital-strategy.ec.europa.eu/en/library/guidelines-transparency-obligations-providers-and-deployers-ai-systems
- FAQ zu Artikel 50: https://digital-strategy.ec.europa.eu/en/faqs/transparency-obligations-under-article-50-ai-act
- EU-Icons: https://digital-strategy.ec.europa.eu/en/policies/eu-icons-labelling-ai-generated-content
- Code of Practice: https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content
- Kommissionsbewertung des Code of Practice: https://digital-strategy.ec.europa.eu/en/library/commission-opinion-assessment-code-practice-transparency-ai-generated-content
## Verbleibende rechtliche Grenze
Der Standard kann Transparenz- und Dokumentationsprozesse unterstützen. Er kann nicht automatisch prüfen oder erfüllen, ob beispielsweise Artikel 4 (AI Literacy), Artikel 5 (verbotene Praktiken), Hochrisiko-Pflichten, GPAI-Pflichten, Datenschutz-, Urheber-, Medien- oder Verbraucherrecht im konkreten Einsatzfall greifen.
Daher sollte öffentlich weiterhin nicht pauschal mit „EU AI Act compliant“ oder „erfüllt alle Anforderungen des EU AI Act“ geworben werden.

View File

@@ -16,7 +16,7 @@ Initialization is performed in `internal/app/server.go`. The product ID and embe
```go
licenses := licenseclient.New(ctx, licenseclient.Config{
Product: "ai-disclosure-standard",
ClientVersion: "1.6.2",
ClientVersion: "1.6.3",
Token: cfg.LicenseToken,
TrustStore: trustStore,
BaseURL: cfg.BaseURL,

View File

@@ -119,7 +119,7 @@ Anfrage:
"baseUrl": "https://ai.example.org",
"host": "ai.example.org",
"instanceId": "production-eu-1",
"clientVersion": "1.6.2"
"clientVersion": "1.6.3"
}
```

View File

@@ -9,7 +9,7 @@ import (
const (
ProductID = "ai-disclosure-standard"
ProductVersion = "1.6.2"
ProductVersion = "1.6.3"
FeatureCustomText = "custom_text"
FeatureCustomBadge = "custom_badge"
FeatureWhiteLabel = "white_label"

View File

@@ -44,6 +44,7 @@ type Server struct {
type option struct{ Value, Label string }
type fact struct{ Label, Value, Link string }
type componentRow struct{ Name, Extent, Activities, Review, Note string }
type legalAssessmentView struct{ Heading, Text, Level string }
type languageLink struct {
Code, Name, URL, AbsoluteURL string
Current bool
@@ -72,6 +73,7 @@ type pageData struct {
Facts []fact
ComponentRows []componentRow
Summary []string
LegalAssessment legalAssessmentView
IsArticle bool
License licenseclient.Status
CustomText bool
@@ -300,9 +302,13 @@ func (s *Server) renderBadge(w http.ResponseWriter, r *http.Request, q url.Value
extent := strings.TrimSpace(q.Get("extent"))
presetID := strings.TrimSpace(q.Get("preset"))
isArticle := q.Get("mode") == "article"
if isArticle {
if d, err := s.declarationFromQuery(q); err == nil {
extent = overallExtent(d)
var parsedDeclaration *declaration.Declaration
if isArticle || strings.TrimSpace(q.Get("legalContext")) != "" {
if parsed, err := s.declarationFromQuery(q); err == nil {
parsedDeclaration = &parsed
if isArticle {
extent = overallExtent(parsed)
}
}
}
if p, ok := declaration.Presets[presetID]; ok && extent == "" {
@@ -336,6 +342,16 @@ func (s *Server) renderBadge(w http.ResponseWriter, r *http.Request, q url.Value
if v := firstNonEmpty(q.Get("badgeMessage"), q.Get("message")); v != "" {
message = v
}
// When the supplied legal context indicates a likely Article 50(4) disclosure
// case, keep the first-layer wording explicit even for Pro-customised badges.
// Custom colours and the left label remain available, but an ambiguous custom
// message must not silently replace the generated disclosure wording.
if parsedDeclaration != nil {
assessment := assessLegalContext(*parsedDeclaration, locale)
if assessment.RequiresDisclosure {
message = assessment.BadgeMessage
}
}
link := strings.TrimSpace(q.Get("link"))
if link == "auto" {
copy := cloneValues(q)
@@ -400,7 +416,7 @@ func (s *Server) handleDeclaration(w http.ResponseWriter, r *http.Request) {
data := pageData{
Name: s.cfg.PublicName, BaseURL: s.cfg.BaseURL, ContactURL: s.cfg.ContactURL, Lang: d.Language, Text: locale.Text, Languages: i18n.Languages(),
Declaration: d, Title: title, Description: description, BadgeURL: badgeURL, ManifestURL: manifestURL, CanonicalURL: canonicalURL, JSONLD: template.JS(jsonLD),
Facts: declarationFacts(d, locale), ComponentRows: declarationComponentRows(d, locale), Summary: declarationSummary(d, locale), IsArticle: isArticle, License: s.licenses.Status(),
Facts: declarationFacts(d, locale), ComponentRows: declarationComponentRows(d, locale), Summary: declarationSummary(d, locale), LegalAssessment: legalAssessmentPage(d, locale), IsArticle: isArticle, License: s.licenses.Status(),
LanguageLinks: languageLinks, DefaultLanguageURL: defaultLanguageURL, LegalNav: legalNavFor(d.Language),
}
w.Header().Set("Link", "<"+manifestURL+">; rel=describedby; type=application/ld+json")
@@ -678,6 +694,24 @@ func declarationFacts(d declaration.Declaration, locale i18n.Locale) []fact {
}
if d.EditorialResponsibility != nil && d.EditorialResponsibility.Name != "" {
facts = append(facts, fact{Label: locale.Text["fact_responsibility"], Value: d.EditorialResponsibility.Name, Link: d.EditorialResponsibility.URL})
if d.EditorialResponsibility.Role != "" {
role := locale.Text["responsibility_"+d.EditorialResponsibility.Role]
if role == "" {
role = d.EditorialResponsibility.Role
}
facts = append(facts, fact{Label: locale.Text["fact_responsibility_type"], Value: role})
}
}
if d.LegalContext != nil && len(d.LegalContext.Categories) > 0 {
labels := make([]string, 0, len(d.LegalContext.Categories))
for _, category := range d.LegalContext.Categories {
if label := legalCategoryLabel(locale, category); label != "" {
labels = append(labels, label)
}
}
if len(labels) > 0 {
facts = append(facts, fact{Label: locale.Text["fact_legal_context"], Value: strings.Join(labels, "; ")})
}
}
if d.DeclaredAt != "" {
facts = append(facts, fact{Label: locale.Text["fact_declared_at"], Value: d.DeclaredAt})
@@ -765,11 +799,11 @@ func declarationSummary(d declaration.Declaration, locale i18n.Locale) []string
closing := map[string][]string{
"de": {
"Sämtliche Angaben werden unter einem gemeinsamen Link veröffentlicht und stehen zusätzlich als maschinenlesbares JSON-LD zur Verfügung.",
"Die inhaltliche und redaktionelle Verantwortung verbleibt unabhängig vom ausgewiesenen KI-Anteil bei der veröffentlichenden Person oder Organisation.",
"Eine redaktionelle Verantwortung im Sinne von Art. 50 Abs. 4 wird nur dann als angegeben behandelt, wenn sie in den Metadaten ausdrücklich benannt ist.",
},
"en": {
"All information is published under a single link and is also available as machine-readable JSON-LD.",
"Responsibility for the content and its editorial publication remains with the publishing person or organisation, irrespective of the stated level of AI involvement.",
"Editorial responsibility for the purposes of Article 50(4) is treated as stated only where it is expressly named in the metadata.",
},
"fr": {
"Toutes les informations sont publiées sous un lien unique et sont également disponibles au format JSON-LD lisible par machine.",
@@ -801,6 +835,80 @@ func declarationSummary(d declaration.Declaration, locale i18n.Locale) []string
return []string{strings.Join(statements, " "), strings.Join(end, " ")}
}
type legalAssessment struct {
RequiresDisclosure bool
BadgeMessage string
Text string
Level string
}
func legalCategorySet(d declaration.Declaration) map[string]bool {
out := map[string]bool{}
if d.LegalContext == nil {
return out
}
for _, category := range d.LegalContext.Categories {
out[category] = true
}
return out
}
func legalCategoryLabel(locale i18n.Locale, category string) string {
switch category {
case "deepfake":
return locale.Text["legal_deepfake"]
case "publicInterestText":
return locale.Text["legal_public_interest"]
case "artisticCreativeSatiricalFictional":
return locale.Text["legal_creative"]
case "otherVoluntary":
return locale.Text["legal_other_voluntary"]
default:
return category
}
}
func hasSubstantiveTextReview(d declaration.Declaration) bool {
text, ok := d.Components["text"]
if !ok {
return false
}
return text.HumanReview == "editorial" || text.HumanReview == "expert"
}
func hasExplicitEditorialResponsibility(d declaration.Declaration) bool {
return d.EditorialResponsibility != nil && strings.TrimSpace(d.EditorialResponsibility.Name) != ""
}
func assessLegalContext(d declaration.Declaration, locale i18n.Locale) legalAssessment {
categories := legalCategorySet(d)
if len(categories) == 0 {
return legalAssessment{Text: locale.Text["legal_assessment_none"], Level: "info"}
}
if categories["otherVoluntary"] {
return legalAssessment{Text: locale.Text["legal_assessment_voluntary"], Level: "info"}
}
if categories["deepfake"] {
text := locale.Text["legal_assessment_deepfake"]
if categories["artisticCreativeSatiricalFictional"] {
text = locale.Text["legal_assessment_deepfake_creative"]
}
return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_legal_disclosure"], Text: text, Level: "attention"}
}
if categories["publicInterestText"] {
if hasSubstantiveTextReview(d) && hasExplicitEditorialResponsibility(d) {
return legalAssessment{Text: locale.Text["legal_assessment_public_exemption"], Level: "info"}
}
return legalAssessment{RequiresDisclosure: true, BadgeMessage: locale.Text["badge_public_interest"], Text: locale.Text["legal_assessment_public_required"], Level: "attention"}
}
return legalAssessment{Text: locale.Text["legal_assessment_voluntary"], Level: "info"}
}
func legalAssessmentPage(d declaration.Declaration, locale i18n.Locale) legalAssessmentView {
a := assessLegalContext(d, locale)
return legalAssessmentView{Heading: locale.Text["legal_assessment_heading"], Text: a.Text, Level: a.Level}
}
func componentSummaryStatement(locale i18n.Locale, name string, component declaration.Component) string {
componentName := locale.Components[name]
if componentName == "" {
@@ -1130,41 +1238,181 @@ const declarationSchema = `{
"title": "AI Usage Declaration",
"type": "object",
"additionalProperties": false,
"required": ["@context", "@type", "schemaVersion", "language", "components", "assurance"],
"required": [
"@context",
"@type",
"schemaVersion",
"language",
"components",
"assurance"
],
"properties": {
"@context": {"type": "string"},
"@type": {"const": "AIUsageDeclaration"},
"schemaVersion": {"const": "1.1"},
"subject": {"type": "string", "format": "uri"},
"declaredAt": {"type": "string", "format": "date-time"},
"language": {"enum": ["de", "en", "fr", "es", "it", "nl", "pt", "pl"]},
"assurance": {"enum": ["selfDeclared", "technicallyRecorded", "signed", "verified"]},
"editorialResponsibility": {
"type": "object", "additionalProperties": false,
"properties": {"name": {"type": "string"}, "url": {"type": "string", "format": "uri"}}
"@context": {
"type": "string"
},
"presentation": {
"type": "object", "additionalProperties": false,
"properties": {
"title": {"type": "string", "maxLength": 120},
"description": {"type": "string", "maxLength": 500},
"badgeLabel": {"type": "string", "maxLength": 40},
"badgeMessage": {"type": "string", "maxLength": 80},
"leftColor": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
"rightColor": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"}
"@type": {
"const": "AIUsageDeclaration"
},
"schemaVersion": {
"const": "1.2"
},
"subject": {
"type": "string",
"format": "uri"
},
"declaredAt": {
"type": "string",
"format": "date-time"
},
"language": {
"enum": [
"de",
"en",
"fr",
"es",
"it",
"nl",
"pt",
"pl"
]
},
"components": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"$ref": "#/$defs/component"
}
},
"components": {"type": "object", "minProperties": 1, "additionalProperties": {"$ref": "#/$defs/component"}}
"editorialResponsibility": {
"type": "object",
"additionalProperties": false,
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"assumed": {
"type": "boolean"
},
"role": {
"enum": [
"publisher",
"other"
]
}
}
},
"assurance": {
"enum": [
"selfDeclared",
"technicallyRecorded",
"signed",
"verified"
]
},
"presentation": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string",
"maxLength": 120
},
"description": {
"type": "string",
"maxLength": 500
},
"badgeLabel": {
"type": "string",
"maxLength": 40
},
"badgeMessage": {
"type": "string",
"maxLength": 80
},
"leftColor": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$"
},
"rightColor": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$"
}
}
},
"legalContext": {
"type": "object",
"additionalProperties": false,
"properties": {
"categories": {
"type": "array",
"uniqueItems": true,
"items": {
"enum": [
"deepfake",
"publicInterestText",
"artisticCreativeSatiricalFictional",
"otherVoluntary"
]
}
}
}
}
},
"$defs": {
"component": {
"type": "object", "additionalProperties": false,
"required": ["aiExtent", "humanReview"],
"type": "object",
"additionalProperties": false,
"required": [
"aiExtent",
"humanReview"
],
"properties": {
"aiExtent": {"enum": ["none", "assisted", "partial", "mostly", "full"]},
"activities": {"type": "array", "uniqueItems": true, "items": {"enum": ["research", "summarisation", "drafting", "generation", "translation", "editing", "imageGeneration", "codeGeneration", "transcription", "classification"]}},
"humanReview": {"enum": ["none", "basic", "editorial", "expert"]},
"note": {"type": "string", "maxLength": 500}
"aiExtent": {
"enum": [
"none",
"assisted",
"partial",
"mostly",
"full"
]
},
"activities": {
"type": "array",
"uniqueItems": true,
"items": {
"enum": [
"research",
"summarisation",
"drafting",
"generation",
"translation",
"editing",
"imageGeneration",
"codeGeneration",
"transcription",
"classification"
]
}
},
"humanReview": {
"enum": [
"none",
"basic",
"editorial",
"expert"
]
},
"note": {
"type": "string",
"maxLength": 500
}
}
}
}
@@ -1184,6 +1432,10 @@ const jsonLDContext = `{
"humanReview": "__BASE_URL__/vocab/humanReview",
"assurance": "__BASE_URL__/vocab/assurance",
"presentation": "__BASE_URL__/vocab/presentation",
"editorialResponsibility": "https://schema.org/accountablePerson"
"editorialResponsibility": "https://schema.org/accountablePerson",
"assumed": "__BASE_URL__/vocab/assumed",
"role": "__BASE_URL__/vocab/responsibilityRole",
"legalContext": "__BASE_URL__/vocab/legalContext",
"categories": "__BASE_URL__/vocab/legalCategories"
}
}`

View File

@@ -258,6 +258,10 @@ func TestGeneratorShowsAIACTGuardrails(t *testing.T) {
}
body := w.Body.String()
for _, expected := range []string{
`id="legal-deepfake"`,
`id="legal-public-interest"`,
`id="legal-creative"`,
`id="responsible-role"`,
`id="responsible"`,
`id="responsible-url"`,
"formale Prüfung",
@@ -532,3 +536,42 @@ func TestGeneratorOffersEmojiTheme(t *testing.T) {
t.Fatalf("generator missing %q", expected)
}
}
func TestPublicInterestBadgeUsesExplicitDisclosureWithoutExemption(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=full&textReview=none&legalContext=publicInterestText&lang=de", nil)
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "KI-generierte / bearbeitete Inhalte") {
t.Fatalf("missing explicit legal disclosure wording: %s", w.Body.String())
}
}
func TestPublicInterestBadgeCanRemainVoluntaryWithSubstantiveReviewAndResponsibility(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/v1/badge.svg?mode=article&textExtent=partial&textReview=expert&legalContext=publicInterestText&responsible=Beispielredaktion&lang=de", nil)
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "Artikeltransparenz") {
t.Fatalf("expected voluntary article wording where possible exception is documented: %s", w.Body.String())
}
}
func TestDeepfakeDeclarationShowsCautiousAssessment(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/declaration?mode=article&imageExtent=full&imageReview=none&legalContext=deepfake&lang=de", nil)
w := httptest.NewRecorder()
testHandler(t).ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("status %d: %s", w.Code, w.Body.String())
}
body := w.Body.String()
for _, expected := range []string{"Vorsichtige Art.-50-Einschätzung", "spricht vieles dafür", "Deepfake / realitätsähnliche KI-Manipulation"} {
if !strings.Contains(body, expected) {
t.Fatalf("missing %q: %s", expected, body)
}
}
}

View File

@@ -13,8 +13,9 @@ import (
)
const (
SchemaVersion = "1.1"
LegacySchemaVersion = "1.0"
SchemaVersion = "1.2"
PreviousSchemaVersion = "1.1"
LegacySchemaVersion = "1.0"
)
var (
@@ -31,8 +32,14 @@ type Component struct {
}
type Responsibility struct {
Name string `json:"name"`
URL string `json:"url,omitempty"`
Assumed bool `json:"assumed,omitempty"`
Role string `json:"role,omitempty"`
Name string `json:"name"`
URL string `json:"url,omitempty"`
}
type LegalContext struct {
Categories []string `json:"categories,omitempty"`
}
type Presentation struct {
@@ -53,6 +60,7 @@ type Declaration struct {
Language string `json:"language"`
Components map[string]Component `json:"components"`
EditorialResponsibility *Responsibility `json:"editorialResponsibility,omitempty"`
LegalContext *LegalContext `json:"legalContext,omitempty"`
Assurance string `json:"assurance"`
Presentation *Presentation `json:"presentation,omitempty"`
}
@@ -90,6 +98,8 @@ var validActivities = map[string]bool{
"transcription": true, "classification": true,
}
var validComponents = map[string]bool{"text": true, "coverImage": true, "image": true, "research": true, "translation": true, "audio": true, "video": true, "code": true, "other": true}
var validLegalCategories = map[string]bool{"deepfake": true, "publicInterestText": true, "artisticCreativeSatiricalFictional": true, "otherVoluntary": true}
var validResponsibilityRoles = map[string]bool{"publisher": true, "other": true}
func NewFromQuery(values url.Values, contextURL string) (Declaration, error) {
return NewFromQueryWithOptions(values, contextURL, ParseOptions{DefaultLanguage: "de"})
@@ -193,8 +203,13 @@ func NewFromQueryWithOptions(values url.Values, contextURL string, options Parse
}
responsibleName := clean(values.Get("responsible"), 200)
responsibleURL := clean(values.Get("responsibleUrl"), 2048)
if responsibleName != "" || responsibleURL != "" {
d.EditorialResponsibility = &Responsibility{Name: responsibleName, URL: responsibleURL}
responsibleRole := clean(values.Get("responsibleRole"), 24)
if responsibleName != "" || responsibleURL != "" || responsibleRole != "" {
d.EditorialResponsibility = &Responsibility{Assumed: responsibleName != "", Role: responsibleRole, Name: responsibleName, URL: responsibleURL}
}
legalCategories := splitCSV(values.Get("legalContext"))
if len(legalCategories) > 0 {
d.LegalContext = &LegalContext{Categories: legalCategories}
}
presentation := &Presentation{
@@ -222,9 +237,16 @@ func Validate(d Declaration) error {
var problems []string
switch d.SchemaVersion {
case SchemaVersion:
case PreviousSchemaVersion:
if d.LegalContext != nil {
problems = append(problems, "legalContext requires schemaVersion 1.2")
}
case LegacySchemaVersion:
if d.Presentation != nil {
problems = append(problems, "presentation requires schemaVersion 1.1")
problems = append(problems, "presentation requires schemaVersion 1.1 or later")
}
if d.LegalContext != nil {
problems = append(problems, "legalContext requires schemaVersion 1.2")
}
if d.Language != "de" && d.Language != "en" {
problems = append(problems, "schemaVersion 1.0 supports only de and en")
@@ -258,12 +280,47 @@ func Validate(d Declaration) error {
if strings.TrimSpace(d.EditorialResponsibility.Name) == "" {
problems = append(problems, "editorialResponsibility.name is required when editorialResponsibility is present")
}
if d.EditorialResponsibility.Role != "" && !validResponsibilityRoles[d.EditorialResponsibility.Role] {
problems = append(problems, "editorialResponsibility.role must be publisher or other")
}
if d.EditorialResponsibility.URL != "" {
if u, err := url.ParseRequestURI(d.EditorialResponsibility.URL); err != nil || u.Scheme == "" || u.Host == "" {
problems = append(problems, "editorialResponsibility.url must be an absolute URL")
}
}
}
if d.LegalContext != nil {
seenLegal := map[string]bool{}
for _, category := range d.LegalContext.Categories {
if !validLegalCategories[category] {
problems = append(problems, "invalid legalContext category: "+category)
}
if seenLegal[category] {
problems = append(problems, "duplicate legalContext category: "+category)
}
seenLegal[category] = true
}
if seenLegal["otherVoluntary"] && len(seenLegal) > 1 {
problems = append(problems, "otherVoluntary cannot be combined with other legalContext categories")
}
if seenLegal["publicInterestText"] {
text, ok := d.Components["text"]
if !ok || text.AIExtent == "none" {
problems = append(problems, "publicInterestText requires a text component with AI involvement")
}
}
if seenLegal["deepfake"] {
hasAIMedia := false
for _, name := range []string{"coverImage", "image", "audio", "video"} {
if c, ok := d.Components[name]; ok && c.AIExtent != "none" {
hasAIMedia = true
}
}
if !hasAIMedia {
problems = append(problems, "deepfake requires an image, audio or video component with AI involvement")
}
}
}
for name, c := range d.Components {
if !validComponents[name] {
problems = append(problems, "invalid component: "+name)

View File

@@ -126,3 +126,39 @@ func TestEditorialResponsibilityRequiresName(t *testing.T) {
t.Fatalf("expected editorial responsibility name validation error, got %v", err)
}
}
func TestLegalContextPublicInterestRequiresAIText(t *testing.T) {
_, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"none"},
"legalContext": {"publicInterestText"},
}, "https://example.org/context/v1")
if err == nil || !strings.Contains(err.Error(), "publicInterestText requires a text component with AI involvement") {
t.Fatalf("expected public-interest consistency error, got %v", err)
}
}
func TestLegalContextDeepfakeRequiresAIMedia(t *testing.T) {
_, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"full"},
"legalContext": {"deepfake"},
}, "https://example.org/context/v1")
if err == nil || !strings.Contains(err.Error(), "deepfake requires an image, audio or video component with AI involvement") {
t.Fatalf("expected deepfake consistency error, got %v", err)
}
}
func TestLegalContextAndResponsibilityAreSerialized(t *testing.T) {
d, err := NewFromQuery(url.Values{
"mode": {"article"}, "lang": {"de"}, "textExtent": {"partial"}, "textReview": {"expert"},
"legalContext": {"publicInterestText"}, "responsibleRole": {"publisher"}, "responsible": {"Beispielredaktion"},
}, "https://example.org/context/v1")
if err != nil {
t.Fatal(err)
}
if d.SchemaVersion != "1.2" || d.LegalContext == nil || len(d.LegalContext.Categories) != 1 {
t.Fatalf("unexpected legal context: %#v", d)
}
if d.EditorialResponsibility == nil || !d.EditorialResponsibility.Assumed || d.EditorialResponsibility.Role != "publisher" {
t.Fatalf("responsibility not explicitly assumed: %#v", d.EditorialResponsibility)
}
}

View File

@@ -37,7 +37,7 @@ var catalogs = map[string]Locale{
"field_preset": "Preset", "option_custom": "Benutzerdefinierte Struktur", "field_component": "Komponente", "field_extent": "KI-Anteil", "field_review": "Menschliche Prüfung",
"field_activities": "Tätigkeiten", "activities_help": "Kommagetrennte Standardwerte, zum Beispiel research, summarisation oder translation.",
"review_help": "Nur den tatsächlich durchgeführten Prüfprozess angeben. Eine formale Prüfung (z. B. Rechtschreibung) ist keine substanzielle menschliche Prüfung oder redaktionelle Kontrolle im Sinne von Art. 50 Abs. 4 AI Act.",
"field_subject": "URL des gekennzeichneten Inhalts", "field_responsible": "Redaktionell verantwortliche Person/Organisation (optional)", "responsible_placeholder": "Name oder Organisation", "field_responsible_url": "Nachweis/Impressum zur redaktionellen Verantwortung (optional)", "field_language": "Ausgabesprache", "field_theme": "Darstellung", "theme_mono": "Monochrom", "theme_color": "Farbig",
"field_subject": "URL des gekennzeichneten Inhalts", "legal_context_legend": "Rechtlicher Kontext (Selbsteinordnung)", "legal_context_help": "Diese Angaben helfen bei der Art.-50-Einordnung. Sie sind keine automatische Rechtsentscheidung. Mehrere Kategorien können zutreffen.", "legal_deepfake": "Deepfake / realitätsähnliche KI-Manipulation", "legal_deepfake_help": "Nur auswählen, wenn Bild, Audio oder Video bestehenden oder plausibel existierenden Personen, Objekten, Orten, Entitäten oder Ereignissen ähnelt und fälschlich authentisch oder wahr erscheinen kann.", "legal_public_interest": "KI-generierter oder manipulierter Text zu einer Angelegenheit von öffentlichem Interesse", "legal_public_interest_help": "Zum Beispiel Politik, öffentliche Verwaltung, Grundrechte, Sicherheit, Gesundheit, Umwelt, Verbrauchersicherheit oder relevante wirtschaftliche, wissenschaftliche oder kulturelle Entwicklungen.", "legal_creative": "Evident künstlerisch / kreativ / satirisch / fiktional", "legal_creative_help": "Bei Deepfakes in solchen Werken kann die Offenlegung in einer angemessenen Form erfolgen, die Darstellung oder Genuss des Werks nicht beeinträchtigt.", "legal_other_voluntary": "Andere / freiwillige Transparenz", "legal_assessment_heading": "Vorsichtige Art.-50-Einschätzung", "legal_assessment_none": "Keine besondere Art.-50-Kategorie wurde angegeben. Die Kennzeichnung wird als freiwillige Transparenz behandelt.", "legal_assessment_voluntary": "Die Angaben sprechen derzeit für eine freiwillige Transparenzkennzeichnung; daraus folgt keine Aussage, dass sonstige AI-Act-Pflichten nicht gelten.", "legal_assessment_deepfake": "Auf Grundlage der Selbsteinordnung als Deepfake spricht vieles dafür, dass eine klare und unterscheidbare Offenlegung spätestens bei der ersten Exposition erforderlich ist.", "legal_assessment_deepfake_creative": "Der Inhalt ist als Deepfake und zugleich als evident künstlerisch, kreativ, satirisch oder fiktional eingeordnet. Eine Offenlegung bleibt grundsätzlich relevant, kann aber in angemessener Weise erfolgen, die Darstellung oder Genuss des Werks nicht beeinträchtigt.", "legal_assessment_public_required": "Auf Grundlage der Angaben spricht vieles dafür, dass der KI-generierte oder manipulierte Text zu einer Angelegenheit von öffentlichem Interesse klar offengelegt werden sollte. Eine ausreichende substanzielle menschliche Prüfung/redaktionelle Kontrolle zusammen mit ausdrücklich übernommener redaktioneller Verantwortung ist nicht vollständig dokumentiert.", "legal_assessment_public_exemption": "Die Angaben dokumentieren eine substanzielle menschliche Prüfung oder redaktionelle Kontrolle sowie eine ausdrücklich benannte redaktionelle Verantwortung. Daher kann die Ausnahme für bestimmte Texte von öffentlichem Interesse nach Art. 50 Abs. 4 in Betracht kommen. Eine freiwillige Transparenzkennzeichnung bleibt möglich.", "legal_assessment_inconsistent": "Die gewählte rechtliche Kategorie passt nicht vollständig zu den strukturierten Inhaltsangaben. Bitte prüfe KI-Anteil und Inhaltsbestandteile.", "badge_legal_disclosure": "KI-generierter / manipulierter Inhalt", "badge_public_interest": "KI-generierte / bearbeitete Inhalte", "fact_legal_context": "Rechtlicher Kontext (Selbsteinordnung)", "field_responsibility_role": "Art der redaktionellen Verantwortung", "responsibility_none": "Nicht angegeben", "responsibility_publisher": "Veröffentlichende Person/Organisation übernimmt die redaktionelle Verantwortung", "responsibility_other": "Andere verantwortliche Stelle", "field_responsible": "Redaktionell verantwortliche Person/Organisation (optional)", "responsible_placeholder": "Name oder Organisation", "field_responsible_url": "Nachweis/Impressum zur redaktionellen Verantwortung (optional)", "field_language": "Ausgabesprache", "field_theme": "Darstellung", "theme_mono": "Monochrom", "theme_color": "Farbig",
"preview": "Vorschau", "copy_html": "HTML kopieren", "copied": "Kopiert",
"pro_eyebrow": "Pro-Anpassung", "pro_title": "Eigene Texte und Badge-Designs", "pro_enabled": "Diese Instanz besitzt eine gültige Pro-Lizenz. Individuelle Texte und Farben können verwendet werden.",
"pro_locked": "Individuelle Titel, Beschreibungstexte, Badge-Beschriftungen und Farben sind in der Pro-Ausgabe verfügbar.",
@@ -48,11 +48,11 @@ var catalogs = map[string]Locale{
"api_stateless_title": "Zustandslos", "api_stateless_desc": "Keine Sessions und deterministische Antworten. Geeignet für horizontale Skalierung, Reverse Proxies und CDNs.",
"footer_no_legal": "Keine Rechtsberatung.", "back": "Zurück", "declaration_eyebrow": "KI-Nutzungserklärung", "fact_component": "Komponente", "fact_extent": "KI-Anteil",
"fact_activities": "Tätigkeiten", "fact_review": "Menschliche Prüfung", "fact_note": "Hinweis", "fact_assurance": "Nachweisniveau", "fact_subject": "Gekennzeichneter Inhalt",
"fact_responsibility": "Redaktionelle Verantwortung", "fact_declared_at": "Erstellt", "none_value": "keine", "transparency_label": "Transparenzhinweis:",
"fact_responsibility": "Redaktionelle Verantwortung", "fact_responsibility_type": "Rolle der redaktionellen Verantwortung", "fact_declared_at": "Erstellt", "none_value": "keine", "transparency_label": "Transparenzhinweis:",
"transparency_text": "Diese Erklärung gibt die von der veröffentlichenden Person oder Organisation bereitgestellten Angaben zur KI-Nutzung wieder. Sie ersetzt weder eine rechtliche Bewertung noch eine Zertifizierung.",
"review_legal_label": "Menschliche Prüfung:", "review_legal_text": "Ein angegebenes Prüfungsniveau ist eine Selbstauskunft zum tatsächlichen Arbeitsprozess. Es begründet für sich allein keine Ausnahme von einer Kennzeichnungspflicht nach Art. 50 Abs. 4 AI Act; hierfür müssen die gesetzlichen Voraussetzungen, insbesondere substanzielle menschliche Prüfung oder redaktionelle Kontrolle und redaktionelle Verantwortung, tatsächlich erfüllt sein.",
"machine_marking_label": "Art. 50 Abs. 2:", "machine_marking_text": "Das verlinkte JSON-LD ist eine ergänzende Dokumentation. Es ist nicht als Ersatz für die vom Anbieter eines generativen KI-Systems geforderte wirksame, zuverlässige, robuste und interoperable maschinenlesbare Markierung der generierten oder manipulierten Ausgabe gedacht.",
"ai_act_embed_label": "Platzierung nach Art. 50:", "ai_act_embed_text": "Bei einer gesetzlich erforderlichen Offenlegung muss der sichtbare Hinweis klar und unterscheidbar spätestens bei der ersten Exposition zum betroffenen Inhalt erscheinen. Ein Badge nur im Footer oder erst hinter einem späteren Link kann dafür zu spät sein.",
"ai_act_embed_label": "Platzierung nach Art. 50:", "ai_act_embed_text": "Bei einer gesetzlich erforderlichen Offenlegung muss der sichtbare Hinweis klar und unterscheidbar spätestens bei der ersten Exposition zum betroffenen Inhalt erscheinen. Ein Badge nur im Footer oder erst hinter einem späteren Link kann dafür zu spät sein. Bei Deepfake-Medien sollte die Kennzeichnung unmittelbar am Inhalt bzw. als gleichwertiges Overlay umgesetzt werden; der Code of Practice sieht für seine Signatories zudem vor, dass sie beim Teilen oder Download sichtbar bleibt.",
"ai_act_machine_label": "Maschinenlesbarkeit:", "ai_act_machine_text": "Dieses JSON-LD dokumentiert die Nutzung ergänzend, erfüllt aber nicht automatisch die Anbieterpflicht zur technischen Markierung nach Art. 50 Abs. 2 AI Act.",
"emoji_compliance_label": "Emoji-Variante:", "emoji_compliance_text": "Das quadratische Emoji ist ein kompakter Zusatz und kein offizielles EU-Kennzeichnungssymbol. Bei einer gesetzlichen Offenlegung sollte es nicht ohne einen unmittelbar verständlichen Text oder eine gleichwertig klare Kennzeichnung verwendet werden.",
"manifest": "Maschinenlesbares Manifest",
@@ -74,13 +74,13 @@ var catalogs = map[string]Locale{
"standard_eyebrow": "Open disclosure standard", "hero_title": "Disclose how AI contributed to content.", "hero_lead": "A visible SVG badge, a human-readable declaration page and machine-readable JSON-LD without cookies, tracking or a database requirement.",
"generator_eyebrow": "Generator", "generator_title": "Create an embed", "generator_intro": "Choose a template or record AI use systematically by content component. The preview, embed code and machine-readable declaration are generated immediately.",
"field_preset": "Preset", "option_custom": "Custom structure", "field_component": "Component", "field_extent": "AI contribution", "field_review": "Human review", "field_activities": "Activities",
"activities_help": "Comma-separated standard values such as research, summarisation or translation.", "review_help": "Record only the review process that actually took place. A merely formal check (for example spelling or grammar) is not substantive human review or editorial control for the purposes of Article 50(4) AI Act.", "field_subject": "URL of the labelled content", "field_responsible": "Editorially responsible person/organisation (optional)", "responsible_placeholder": "Name or organisation", "field_responsible_url": "Evidence/imprint for editorial responsibility (optional)", "field_language": "Output language", "field_theme": "Appearance", "theme_mono": "Monochrome", "theme_color": "Colour",
"activities_help": "Comma-separated standard values such as research, summarisation or translation.", "review_help": "Record only the review process that actually took place. A merely formal check (for example spelling or grammar) is not substantive human review or editorial control for the purposes of Article 50(4) AI Act.", "field_subject": "URL of the labelled content", "field_responsibility_role": "Editorial responsibility type", "responsibility_none": "Not specified", "responsibility_publisher": "Publishing person/organisation assumes editorial responsibility", "responsibility_other": "Other responsible party", "field_responsible": "Editorially responsible person/organisation (optional)", "responsible_placeholder": "Name or organisation", "field_responsible_url": "Evidence/imprint for editorial responsibility (optional)", "field_language": "Output language", "field_theme": "Appearance", "theme_mono": "Monochrome", "theme_color": "Colour",
"preview": "Preview", "copy_html": "Copy HTML", "copied": "Copied", "pro_eyebrow": "Pro customisation", "pro_title": "Custom copy and badge designs",
"pro_enabled": "This instance has a valid Pro licence. Custom copy and colours are available.", "pro_locked": "Custom titles, descriptions, badge labels and colours are available in the Pro edition.",
"custom_title": "Custom declaration title", "custom_description": "Custom description", "custom_badge_label": "Custom left badge label", "custom_badge_message": "Custom right badge label", "custom_left_color": "Left colour", "custom_right_color": "Right colour", "pro_required": "Pro licence required",
"api_badge_title": "Badge endpoint", "api_badge_desc": "Standard badges are generated from presets and structured parameters. Pro adds custom copy and colours.", "api_manifest_title": "Manifest", "api_manifest_desc": "The JSON-LD manifest can be linked, validated or included in build pipelines.",
"api_stateless_title": "Stateless", "api_stateless_desc": "No sessions and deterministic responses. Designed for horizontal scaling, reverse proxies and CDNs.", "footer_no_legal": "Not legal advice.",
"back": "Back", "declaration_eyebrow": "AI usage declaration", "fact_component": "Component", "fact_extent": "AI contribution", "fact_activities": "Activities", "fact_review": "Human review", "fact_note": "Note", "fact_assurance": "Assurance level", "fact_subject": "Labelled content", "fact_responsibility": "Editorial responsibility", "fact_declared_at": "Declared at", "none_value": "none", "transparency_label": "Transparency notice:", "transparency_text": "This declaration presents the information on AI use supplied by the publishing person or organisation. It does not replace a legal assessment or certification.", "review_legal_label": "Human review:", "review_legal_text": "A stated review level is a self-declaration of the process that actually took place. By itself it does not establish an exemption from a labelling duty under Article 50(4) AI Act; the statutory conditions, including substantive human review or editorial control and editorial responsibility, must actually be met.", "machine_marking_label": "Article 50(2):", "machine_marking_text": "The linked JSON-LD is supplementary documentation. It is not intended to replace the effective, reliable, robust and interoperable machine-readable marking of generated or manipulated output required from the provider of an in-scope generative AI system.", "ai_act_embed_label": "Article 50 placement:", "ai_act_embed_text": "Where disclosure is legally required, the visible notice must be clear and distinguishable no later than the first exposure to the affected content. A badge shown only in a footer or behind a later link may be too late.", "ai_act_machine_label": "Machine readability:", "ai_act_machine_text": "This JSON-LD supplements documentation but does not automatically satisfy the provider-side technical marking duty under Article 50(2) AI Act.", "emoji_compliance_label": "Emoji variant:", "emoji_compliance_text": "The square emoji is a compact supplement and is not an official EU labelling icon. Where a legal disclosure is required, do not rely on it without immediately understandable text or an equivalently clear disclosure.", "manifest": "Machine-readable manifest", "ai_label": "AI use", "capabilities": "Capabilities",
"back": "Back", "declaration_eyebrow": "AI usage declaration", "fact_component": "Component", "fact_extent": "AI contribution", "fact_activities": "Activities", "fact_review": "Human review", "fact_note": "Note", "fact_assurance": "Assurance level", "fact_subject": "Labelled content", "fact_responsibility": "Editorial responsibility", "fact_responsibility_type": "Editorial responsibility role", "fact_declared_at": "Declared at", "none_value": "none", "transparency_label": "Transparency notice:", "transparency_text": "This declaration presents the information on AI use supplied by the publishing person or organisation. It does not replace a legal assessment or certification.", "review_legal_label": "Human review:", "review_legal_text": "A stated review level is a self-declaration of the process that actually took place. By itself it does not establish an exemption from a labelling duty under Article 50(4) AI Act; the statutory conditions, including substantive human review or editorial control and editorial responsibility, must actually be met.", "machine_marking_label": "Article 50(2):", "machine_marking_text": "The linked JSON-LD is supplementary documentation. It is not intended to replace the effective, reliable, robust and interoperable machine-readable marking of generated or manipulated output required from the provider of an in-scope generative AI system.", "ai_act_embed_label": "Article 50 placement:", "ai_act_embed_text": "Where disclosure is legally required, the visible notice must be clear and distinguishable no later than first exposure to the affected content. A badge shown only in a footer or behind a later link may be too late. For deepfake media, the notice should be attached directly to the content or provided through an equivalent overlay; the Code of Practice additionally provides for visibility when content is reshared or downloaded for its signatories.", "ai_act_machine_label": "Machine readability:", "ai_act_machine_text": "This JSON-LD supplements documentation but does not automatically satisfy the provider-side technical marking duty under Article 50(2) AI Act.", "emoji_compliance_label": "Emoji variant:", "emoji_compliance_text": "The square emoji is a compact supplement and is not an official EU labelling icon. Where a legal disclosure is required, do not rely on it without immediately understandable text or an equivalently clear disclosure.", "manifest": "Machine-readable manifest", "ai_label": "AI use", "capabilities": "Capabilities",
},
map[string]PresetText{"no-ai": {"No AI use", "According to the information supplied, no generative AI was used to create the labelled content."}, "research": {"Research assistance", "AI was used to support research or identify relevant sources. Human review is shown only when explicitly selected."}, "summary": {"Content summarisation", "AI was used to summarise source material. Human review or editorial control is not presumed automatically."}, "full": {"Full content generation", "The labelled content was created mostly or entirely with generative AI. Any actual human review process must be stated separately."}},
map[string]string{"none": "No AI use", "assisted": "AI-assisted", "partial": "Partially AI-generated", "mostly": "Mostly AI-generated", "full": "Fully AI-generated"},

View File

@@ -183,13 +183,13 @@ func Build(lang, productName, baseURL, salesURL, contactURL string, prices Price
# Adjust BASE_URL and PUBLIC_NAME in .env
docker compose up -d --build
curl -fsS http://localhost:8080/readyz`},
{ID: "docker", Title: copy.InstallTitles[1], Summary: copy.InstallSummaries[1], Code: `docker build -t ai-disclosure-standard:1.6.2 .
{ID: "docker", Title: copy.InstallTitles[1], Summary: copy.InstallSummaries[1], Code: `docker build -t ai-disclosure-standard:1.6.3 .
docker run -d --name ai-disclosure \
-p 8080:8080 \
-e BASE_URL=https://ai.example.org \
-e PUBLIC_NAME="AI Usage Disclosure" \
--read-only --tmpfs /tmp \
ai-disclosure-standard:1.6.2`},
ai-disclosure-standard:1.6.3`},
{ID: "kubernetes", Title: copy.InstallTitles[2], Summary: copy.InstallSummaries[2], Code: `# Image, Domain und TLS-Secret in deploy/kubernetes.yaml ersetzen
kubectl create secret generic ai-disclosure-license \
--from-literal=token='...'

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: AI Usage Disclosure API
version: 1.6.2
version: 1.6.3
description: Stateless multilingual badge, declaration and validation API with optional licensed presentation capabilities.
servers:
- url: https://ai.example.org
@@ -31,6 +31,7 @@ paths:
- {$ref: '#/components/parameters/extent'}
- {$ref: '#/components/parameters/language'}
- {name: preset, in: query, schema: {type: string, enum: [no-ai, research, summary, full]}}
- {$ref: '#/components/parameters/legalContext'}
- {$ref: '#/components/parameters/theme'}
- {$ref: '#/components/parameters/style'}
- {name: link, in: query, schema: {type: string}}
@@ -54,6 +55,11 @@ paths:
- {$ref: '#/components/parameters/component'}
- {$ref: '#/components/parameters/activities'}
- {$ref: '#/components/parameters/review'}
- {$ref: '#/components/parameters/legalContext'}
- {name: responsibleRole, in: query, schema: {type: string, enum: [publisher, other]}}
- {name: responsible, in: query, schema: {type: string, maxLength: 200}}
- {name: responsibleUrl, in: query, schema: {type: string, format: uri}}
- {$ref: '#/components/parameters/legalContext'}
- {$ref: '#/components/parameters/assurance'}
- {$ref: '#/components/parameters/customTitle'}
- {$ref: '#/components/parameters/customDescription'}
@@ -75,6 +81,7 @@ paths:
- {$ref: '#/components/parameters/activities'}
- {$ref: '#/components/parameters/review'}
- {name: subject, in: query, schema: {type: string, format: uri}}
- {name: responsibleRole, in: query, schema: {type: string, enum: [publisher, other]}}
- {name: responsible, in: query, schema: {type: string, maxLength: 200}}
- {name: responsibleUrl, in: query, schema: {type: string, format: uri}}
- {$ref: '#/components/parameters/assurance'}
@@ -163,7 +170,12 @@ components:
review:
name: review
in: query
schema: {type: string, enum: [none, basic, editorial, expert], default: editorial}
schema: {type: string, enum: [none, basic, editorial, expert], default: none}
legalContext:
name: legalContext
in: query
description: Comma-separated self-classification for Article 50 context. This is supporting metadata, not an automatic legal determination.
schema: {type: string, example: publicInterestText}
assurance:
name: assurance
in: query

View File

@@ -4,35 +4,130 @@
"title": "AI Usage Declaration",
"type": "object",
"additionalProperties": false,
"required": ["@context", "@type", "schemaVersion", "language", "components", "assurance"],
"required": [
"@context",
"@type",
"schemaVersion",
"language",
"components",
"assurance"
],
"properties": {
"@context": {"type": "string"},
"@type": {"const": "AIUsageDeclaration"},
"schemaVersion": {"const": "1.1"},
"subject": {"type": "string", "format": "uri"},
"declaredAt": {"type": "string", "format": "date-time"},
"language": {"enum": ["de", "en", "fr", "es", "it", "nl", "pt", "pl"]},
"components": {"type": "object", "minProperties": 1, "additionalProperties": {"$ref": "#/$defs/component"}},
"@context": {
"type": "string"
},
"@type": {
"const": "AIUsageDeclaration"
},
"schemaVersion": {
"const": "1.2"
},
"subject": {
"type": "string",
"format": "uri"
},
"declaredAt": {
"type": "string",
"format": "date-time"
},
"language": {
"enum": [
"de",
"en",
"fr",
"es",
"it",
"nl",
"pt",
"pl"
]
},
"components": {
"type": "object",
"minProperties": 1,
"additionalProperties": {
"$ref": "#/$defs/component"
}
},
"editorialResponsibility": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"required": [
"name"
],
"properties": {
"name": {"type": "string"},
"url": {"type": "string", "format": "uri"}
"name": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"assumed": {
"type": "boolean"
},
"role": {
"enum": [
"publisher",
"other"
]
}
}
},
"assurance": {"enum": ["selfDeclared", "technicallyRecorded", "signed", "verified"]},
"assurance": {
"enum": [
"selfDeclared",
"technicallyRecorded",
"signed",
"verified"
]
},
"presentation": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {"type": "string", "maxLength": 120},
"description": {"type": "string", "maxLength": 500},
"badgeLabel": {"type": "string", "maxLength": 40},
"badgeMessage": {"type": "string", "maxLength": 80},
"leftColor": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
"rightColor": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"}
"title": {
"type": "string",
"maxLength": 120
},
"description": {
"type": "string",
"maxLength": 500
},
"badgeLabel": {
"type": "string",
"maxLength": 40
},
"badgeMessage": {
"type": "string",
"maxLength": 80
},
"leftColor": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$"
},
"rightColor": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$"
}
}
},
"legalContext": {
"type": "object",
"additionalProperties": false,
"properties": {
"categories": {
"type": "array",
"uniqueItems": true,
"items": {
"enum": [
"deepfake",
"publicInterestText",
"artisticCreativeSatiricalFictional",
"otherVoluntary"
]
}
}
}
}
},
@@ -40,16 +135,50 @@
"component": {
"type": "object",
"additionalProperties": false,
"required": ["aiExtent", "humanReview"],
"required": [
"aiExtent",
"humanReview"
],
"properties": {
"aiExtent": {"enum": ["none", "assisted", "partial", "mostly", "full"]},
"aiExtent": {
"enum": [
"none",
"assisted",
"partial",
"mostly",
"full"
]
},
"activities": {
"type": "array",
"uniqueItems": true,
"items": {"enum": ["research", "summarisation", "drafting", "generation", "translation", "editing", "imageGeneration", "codeGeneration", "transcription", "classification"]}
"items": {
"enum": [
"research",
"summarisation",
"drafting",
"generation",
"translation",
"editing",
"imageGeneration",
"codeGeneration",
"transcription",
"classification"
]
}
},
"humanReview": {"enum": ["none", "basic", "editorial", "expert"]},
"note": {"type": "string", "maxLength": 500}
"humanReview": {
"enum": [
"none",
"basic",
"editorial",
"expert"
]
},
"note": {
"type": "string",
"maxLength": 500
}
}
}
}

View File

@@ -35,6 +35,36 @@
});
}
function legalCategories() {
const values = [];
if ($('legal-deepfake')?.checked) values.push('deepfake');
if ($('legal-public-interest')?.checked) values.push('publicInterestText');
if ($('legal-creative')?.checked) values.push('artisticCreativeSatiricalFictional');
if ($('legal-other-voluntary')?.checked) values.push('otherVoluntary');
return values;
}
function substantiveReview(mode) {
return mode === 'editorial' || mode === 'expert';
}
function legalAssessment(locale, articleMode) {
const categories = legalCategories();
const deepfake = categories.includes('deepfake');
const publicInterest = categories.includes('publicInterestText');
const creative = categories.includes('artisticCreativeSatiricalFictional');
if (categories.length === 0) return {text: locale.text.legal_assessment_none, legal: false, badge: locale.text.badge_article || 'Article transparency'};
if (categories.includes('otherVoluntary')) return {text: locale.text.legal_assessment_voluntary, legal: false, badge: locale.text.badge_article || 'Article transparency'};
if (deepfake) return {text: creative ? locale.text.legal_assessment_deepfake_creative : locale.text.legal_assessment_deepfake, legal: true, badge: locale.text.badge_legal_disclosure || 'AI-generated / manipulated content'};
if (publicInterest) {
const review = articleMode ? $('article-text-review').value : ($('component').value === 'text' ? $('review').value : 'none');
const hasResponsibility = Boolean(value('responsible'));
if (substantiveReview(review) && hasResponsibility) return {text: locale.text.legal_assessment_public_exemption, legal: false, badge: locale.text.badge_article || 'Article transparency'};
return {text: locale.text.legal_assessment_public_required, legal: true, badge: locale.text.badge_public_interest || 'AI-generated / edited content'};
}
return {text: locale.text.legal_assessment_voluntary, legal: false, badge: locale.text.badge_article || 'Article transparency'};
}
function update(applyPreset = false) {
const mode = $('declaration-mode').value;
const articleMode = mode === 'article';
@@ -68,8 +98,11 @@
params.set('lang', lang);
params.set('assurance', assurance);
if ($('subject').value.trim()) params.set('subject', $('subject').value.trim());
if (value('responsible-role')) params.set('responsibleRole', value('responsible-role'));
if (value('responsible')) params.set('responsible', value('responsible'));
if (value('responsible-url')) params.set('responsibleUrl', value('responsible-url'));
const legal = legalCategories();
if (legal.length) params.set('legalContext', legal.join(','));
if (cfg.capabilities.custom_text) {
if (value('custom-title')) params.set('customTitle', value('custom-title'));
@@ -88,15 +121,27 @@
badgeParams.set('link', declarationURL);
const badgeURL = `${base}/v1/badge.svg?${badgeParams}`;
const manifestURL = `${base}/v1/declaration.json?${params}`;
const alt = articleMode
? (locale.text.badge_article || 'Article transparency')
: ((locale.presets[preset] && locale.presets[preset].title) || locale.extents[$('extent').value] || 'AI usage disclosure');
const assessment = legalAssessment(locale, articleMode);
const alt = assessment.legal
? assessment.badge
: (articleMode
? (locale.text.badge_article || 'Article transparency')
: ((locale.presets[preset] && locale.presets[preset].title) || locale.extents[$('extent').value] || 'AI usage disclosure'));
if ($('legal-assessment')) {
$('legal-assessment').innerHTML = `<strong>${locale.text.legal_assessment_heading || 'Article 50 assessment'}</strong><span>${assessment.text || ''}</span>`;
$('legal-assessment').dataset.level = assessment.legal ? 'attention' : 'info';
}
$('preview-badge').src = badgeURL;
$('preview-badge').alt = alt;
$('preview-link').href = declarationURL;
$('html-code').value = `<a href="${declarationURL}"><img src="${badgeURL}" alt="${alt}"></a>`;
$('markdown-code').value = `[![${alt}](${badgeURL})](${declarationURL})`;
const imageMarkup = `<a href="${declarationURL}"><img src="${badgeURL}" alt="${alt}"></a>`;
$('html-code').value = assessment.legal && $('theme').value === 'emoji'
? `${imageMarkup} <span>${assessment.badge}</span>`
: imageMarkup;
$('markdown-code').value = assessment.legal && $('theme').value === 'emoji'
? `[![${alt}](${badgeURL})](${declarationURL}) **${assessment.badge}**`
: `[![${alt}](${badgeURL})](${declarationURL})`;
$('json-code').value = manifestURL;
}
@@ -114,6 +159,52 @@
});
});
function alignLegalContextWithContent(changedId) {
if (changedId === 'legal-deepfake' && $('legal-deepfake')?.checked) {
if ($('legal-other-voluntary')) $('legal-other-voluntary').checked = false;
if ($('declaration-mode').value === 'single') {
if ($('legal-public-interest')?.checked) {
$('declaration-mode').value = 'article';
} else if (!['coverImage', 'image', 'audio', 'video'].includes($('component').value)) {
$('preset').value = '';
$('component').value = 'image';
if ($('extent').value === 'none') $('extent').value = 'partial';
}
}
}
if (changedId === 'legal-public-interest' && $('legal-public-interest')?.checked) {
if ($('legal-other-voluntary')) $('legal-other-voluntary').checked = false;
if ($('declaration-mode').value === 'single') {
if ($('legal-deepfake')?.checked) {
$('declaration-mode').value = 'article';
} else {
$('preset').value = '';
$('component').value = 'text';
if ($('extent').value === 'none') $('extent').value = 'partial';
}
}
if ($('declaration-mode').value === 'article' && $('article-text').value === 'none') {
$('article-text').value = 'partial';
}
}
if (changedId === 'legal-creative' && $('legal-creative')?.checked && $('legal-other-voluntary')) {
$('legal-other-voluntary').checked = false;
}
if ($('declaration-mode').value === 'article' && $('legal-deepfake')?.checked) {
const media = ['article-cover-image', 'article-image'];
const hasMedia = media.some(id => $(id)?.value !== 'none');
if (!hasMedia && $('article-image')) $('article-image').value = 'partial';
}
}
['legal-deepfake', 'legal-public-interest', 'legal-creative'].forEach(id => {
$(id)?.addEventListener('change', () => alignLegalContextWithContent(id));
});
$('legal-other-voluntary')?.addEventListener('change', () => {
if (!$('legal-other-voluntary').checked) return;
['legal-deepfake', 'legal-public-interest', 'legal-creative'].forEach(id => { if ($(id)) $(id).checked = false; });
});
form.addEventListener('input', event => update(event.target.id === 'preset'));
form.addEventListener('change', event => update(event.target.id === 'preset'));
document.querySelector('.pro-panel')?.addEventListener('input', () => update(false));

View File

@@ -59,3 +59,6 @@
line-height: 1.5;
}
.compliance-notice { margin-top: 14px; }
/* Article 50 context and cautious assessment */
.legal-context-builder{border:1px solid var(--line);border-radius:14px;padding:22px;background:#fbfaf7}.legal-context-builder legend{padding:0 8px;font-weight:800}.legal-context-builder>p{margin-top:0;color:var(--muted)}.generator-grid .check-row{display:grid;grid-template-columns:22px 1fr;gap:10px;align-items:start;padding:10px 0;font-weight:400}.check-row input{width:18px;height:18px;margin-top:3px}.check-row span{display:flex;flex-direction:column;gap:3px}.check-row strong{font-weight:800}.legal-assessment{margin-top:14px;border-radius:12px;padding:16px 18px;background:#eeeae2;display:flex;flex-direction:column;gap:6px}.legal-assessment[data-level="attention"]{border-left:4px solid #8f2d1f}.legal-assessment[data-level="info"]{border-left:4px solid #5d5a50}.legal-assessment-section{margin-top:32px}.legal-assessment-card{border:1px solid var(--line);border-radius:14px;padding:20px;background:#fbfaf7}.legal-assessment-card.attention{border-left:5px solid #8f2d1f}.legal-assessment-card.info{border-left:5px solid #5d5a50}

View File

@@ -158,7 +158,7 @@
</section>
</main>
<footer><span>{{.Background.Footer}} · App 1.6.2 · Schema 1.1 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<footer><span>{{.Background.Footer}} · App 1.6.3 · Schema 1.2 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<script src="/static/marketing.js" defer></script>
</body>
</html>

View File

@@ -48,6 +48,13 @@
</div>
</section>
{{if .LegalAssessment.Text}}
<section class="legal-assessment-section" aria-labelledby="legal-assessment-title">
<h2 id="legal-assessment-title">{{.LegalAssessment.Heading}}</h2>
<div class="legal-assessment-card {{.LegalAssessment.Level}}">{{.LegalAssessment.Text}}</div>
</section>
{{end}}
<dl class="facts metadata-facts">
{{range .Facts}}<div><dt>{{.Label}}</dt><dd>{{if .Link}}<a href="{{.Link}}">{{.Value}}</a>{{else}}{{.Value}}{{end}}</dd></div>{{end}}
</dl>

View File

@@ -71,6 +71,15 @@
<div class="article-component"><label>Code<select id="article-code" data-review="article-code-review">{{range .Extents}}<option value="{{.Value}}" {{if eq .Value "none"}}selected{{end}}>{{.Label}}</option>{{end}}</select></label><label>{{index .Text "field_review"}}<select id="article-code-review">{{range .Reviews}}<option value="{{.Value}}" {{if eq .Value "none"}}selected{{end}}>{{.Label}}</option>{{end}}</select></label></div>
</div>
</fieldset>
<fieldset class="legal-context-builder wide">
<legend>{{index .Text "legal_context_legend"}}</legend>
<p>{{index .Text "legal_context_help"}}</p>
<label class="check-row"><input id="legal-deepfake" type="checkbox" value="deepfake"><span><strong>{{index .Text "legal_deepfake"}}</strong><small>{{index .Text "legal_deepfake_help"}}</small></span></label>
<label class="check-row"><input id="legal-public-interest" type="checkbox" value="publicInterestText"><span><strong>{{index .Text "legal_public_interest"}}</strong><small>{{index .Text "legal_public_interest_help"}}</small></span></label>
<label class="check-row"><input id="legal-creative" type="checkbox" value="artisticCreativeSatiricalFictional"><span><strong>{{index .Text "legal_creative"}}</strong><small>{{index .Text "legal_creative_help"}}</small></span></label>
<label class="check-row"><input id="legal-other-voluntary" type="checkbox" value="otherVoluntary"><span><strong>{{index .Text "legal_other_voluntary"}}</strong></span></label>
<div id="legal-assessment" class="legal-assessment" role="status" aria-live="polite"></div>
</fieldset>
<label>{{index .Text "field_language"}}
<select id="lang" name="lang">{{range .Languages}}<option value="{{.Code}}" {{if eq $.Lang .Code}}selected{{end}}>{{.Name}}</option>{{end}}</select>
</label>
@@ -84,12 +93,20 @@
<label class="wide">{{index .Text "field_subject"}}
<input id="subject" name="subject" type="url" placeholder="https://example.org/article">
</label>
<label class="wide">{{index .Text "field_responsibility_role"}}
<select id="responsible-role" name="responsibleRole">
<option value="">{{index .Text "responsibility_none"}}</option>
<option value="publisher">{{index .Text "responsibility_publisher"}}</option>
<option value="other">{{index .Text "responsibility_other"}}</option>
</select>
</label>
<label>{{index .Text "field_responsible"}}
<input id="responsible" name="responsible" maxlength="200" placeholder="{{index .Text "responsible_placeholder"}}">
</label>
<label>{{index .Text "field_responsible_url"}}
<input id="responsible-url" name="responsibleUrl" type="url" placeholder="https://example.org/impressum">
</label>
<p class="form-warning wide">{{index .Text "review_legal_text"}}</p>
</form>
{{if or .CustomText .CustomBadge}}
@@ -143,7 +160,7 @@
<article><p class="eyebrow">HA</p><h2>{{index .Text "api_stateless_title"}}</h2><p>{{index .Text "api_stateless_desc"}}</p></article>
</section>
</main>
<footer><span>AI Usage Disclosure · App 1.6.2 · Schema 1.1 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<footer><span>AI Usage Disclosure · App 1.6.3 · Schema 1.2 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<script nonce="{{.CSPNonce}}">window.APP_CONFIG={{.AppConfig}};</script>
<script src="/static/app.js" defer></script>
</body>

View File

@@ -46,7 +46,7 @@
<img src="/badge/full.svg?lang={{.Lang}}&amp;theme=color" alt="Full generation badge">
<pre><code>{
"@type": "AIUsageDeclaration",
"schemaVersion": "1.1",
"schemaVersion": "1.2",
"assurance": "selfDeclared"
}</code></pre>
</div>
@@ -119,7 +119,7 @@
</section>
</main>
<footer><span>{{.Marketing.Footer}} · App 1.6.2 · Schema 1.1 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<footer><span>{{.Marketing.Footer}} · App 1.6.3 · Schema 1.2 · {{.License.Edition}}</span>{{template "legal-links" .}}</footer>
<script src="/static/marketing.js" defer></script>
</body>
</html>