diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cef1bd..876d49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 88fb973..7b1662d 100644 --- a/Makefile +++ b/Makefile @@ -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 . diff --git a/README.md b/README.md index 35b0a63..c5958b0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/deploy/kubernetes.yaml b/deploy/kubernetes.yaml index 3871207..3aba951 100644 --- a/deploy/kubernetes.yaml +++ b/deploy/kubernetes.yaml @@ -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 diff --git a/deploy/swarm-stack.yaml b/deploy/swarm-stack.yaml index 1ead930..56688dc 100644 --- a/deploy/swarm-stack.yaml +++ b/deploy/swarm-stack.yaml @@ -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" diff --git a/docs/BACKGROUND-PAGE.md b/docs/BACKGROUND-PAGE.md index ff32d05..793e8fd 100644 --- a/docs/BACKGROUND-PAGE.md +++ b/docs/BACKGROUND-PAGE.md @@ -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 diff --git a/docs/EU-AI-ACT-COMPLIANCE.md b/docs/EU-AI-ACT-COMPLIANCE.md index 9556591..3eb1043 100644 --- a/docs/EU-AI-ACT-COMPLIANCE.md +++ b/docs/EU-AI-ACT-COMPLIANCE.md @@ -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. diff --git a/docs/EU-AI-ACT-REVIEW-2026-07-21-V2.md b/docs/EU-AI-ACT-REVIEW-2026-07-21-V2.md new file mode 100644 index 0000000..2c15e5d --- /dev/null +++ b/docs/EU-AI-ACT-REVIEW-2026-07-21-V2.md @@ -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. diff --git a/docs/LICENSE-CLIENT.md b/docs/LICENSE-CLIENT.md index a58a341..c47458a 100644 --- a/docs/LICENSE-CLIENT.md +++ b/docs/LICENSE-CLIENT.md @@ -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, diff --git a/docs/LICENSE-INTEGRATION.md b/docs/LICENSE-INTEGRATION.md index ac0f2a4..5cae9f1 100644 --- a/docs/LICENSE-INTEGRATION.md +++ b/docs/LICENSE-INTEGRATION.md @@ -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" } ``` diff --git a/internal/app/licensing.go b/internal/app/licensing.go index 81b93f5..79499b1 100644 --- a/internal/app/licensing.go +++ b/internal/app/licensing.go @@ -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" diff --git a/internal/app/server.go b/internal/app/server.go index a02c81b..e64f767 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -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" } }` diff --git a/internal/app/server_test.go b/internal/app/server_test.go index 4556d6c..fc72b86 100644 --- a/internal/app/server_test.go +++ b/internal/app/server_test.go @@ -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) + } + } +} diff --git a/internal/declaration/model.go b/internal/declaration/model.go index aa3cc46..89b8b14 100644 --- a/internal/declaration/model.go +++ b/internal/declaration/model.go @@ -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) diff --git a/internal/declaration/model_test.go b/internal/declaration/model_test.go index 4cf8473..28d41cc 100644 --- a/internal/declaration/model_test.go +++ b/internal/declaration/model_test.go @@ -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) + } +} diff --git a/internal/i18n/catalog.go b/internal/i18n/catalog.go index 2fa61f0..39b9bf4 100644 --- a/internal/i18n/catalog.go +++ b/internal/i18n/catalog.go @@ -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"}, diff --git a/internal/marketing/content.go b/internal/marketing/content.go index db5e997..ab52364 100644 --- a/internal/marketing/content.go +++ b/internal/marketing/content.go @@ -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='...' diff --git a/openapi.yaml b/openapi.yaml index 10bb54b..7eac5b1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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 diff --git a/schema/declaration.schema.json b/schema/declaration.schema.json index a989ab1..0c1dea8 100644 --- a/schema/declaration.schema.json +++ b/schema/declaration.schema.json @@ -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 + } } } } diff --git a/web/static/app.js b/web/static/app.js index bdb10ba..ce3e001 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -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 = `${locale.text.legal_assessment_heading || 'Article 50 assessment'}${assessment.text || ''}`; + $('legal-assessment').dataset.level = assessment.legal ? 'attention' : 'info'; + } $('preview-badge').src = badgeURL; $('preview-badge').alt = alt; $('preview-link').href = declarationURL; - $('html-code').value = `${alt}`; - $('markdown-code').value = `[![${alt}](${badgeURL})](${declarationURL})`; + const imageMarkup = `${alt}`; + $('html-code').value = assessment.legal && $('theme').value === 'emoji' + ? `${imageMarkup} ${assessment.badge}` + : 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)); diff --git a/web/static/style.css b/web/static/style.css index 2981126..7c5d781 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -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} diff --git a/web/templates/background.html b/web/templates/background.html index e18237d..01ae117 100644 --- a/web/templates/background.html +++ b/web/templates/background.html @@ -158,7 +158,7 @@ - + diff --git a/web/templates/declaration.html b/web/templates/declaration.html index 843c399..4eead04 100644 --- a/web/templates/declaration.html +++ b/web/templates/declaration.html @@ -48,6 +48,13 @@ + {{if .LegalAssessment.Text}} + + {{end}} +
{{range .Facts}}
{{.Label}}
{{if .Link}}{{.Value}}{{else}}{{.Value}}{{end}}
{{end}}
diff --git a/web/templates/index.html b/web/templates/index.html index 68226ac..ce565da 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -71,6 +71,15 @@
+ @@ -84,12 +93,20 @@ + +

{{index .Text "review_legal_text"}}

{{if or .CustomText .CustomBadge}} @@ -143,7 +160,7 @@

HA

{{index .Text "api_stateless_title"}}

{{index .Text "api_stateless_desc"}}

- + diff --git a/web/templates/marketing.html b/web/templates/marketing.html index 977792c..4f52aa3 100644 --- a/web/templates/marketing.html +++ b/web/templates/marketing.html @@ -46,7 +46,7 @@ Full generation badge
{
   "@type": "AIUsageDeclaration",
-  "schemaVersion": "1.1",
+  "schemaVersion": "1.2",
   "assurance": "selfDeclared"
 }
@@ -119,7 +119,7 @@ - +