RC-5 Versiopn: 1.6.3
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
+1 -1
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"
+286 -34
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"
}
}`
+43
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)
}
}
}
+64 -7
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)
+36
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)
}
}
+5 -5
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"},
+2 -2
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='...'