@sweberdev/witness 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -2
  3. package/dist/ai-sdk.cjs +921 -0
  4. package/dist/ai-sdk.cjs.map +1 -0
  5. package/dist/ai-sdk.d.cts +95 -0
  6. package/dist/ai-sdk.d.ts +95 -0
  7. package/dist/ai-sdk.js +150 -0
  8. package/dist/ai-sdk.js.map +1 -0
  9. package/dist/bin.cjs +944 -0
  10. package/dist/bin.cjs.map +1 -0
  11. package/dist/bin.d.cts +1 -0
  12. package/dist/bin.d.ts +1 -0
  13. package/dist/bin.js +12 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/chunk-APXU3MTX.js +958 -0
  16. package/dist/chunk-APXU3MTX.js.map +1 -0
  17. package/dist/chunk-DHX7SNWE.js +310 -0
  18. package/dist/chunk-DHX7SNWE.js.map +1 -0
  19. package/dist/chunk-L65R2SM5.js +228 -0
  20. package/dist/chunk-L65R2SM5.js.map +1 -0
  21. package/dist/chunk-UBRK4CTM.js +156 -0
  22. package/dist/chunk-UBRK4CTM.js.map +1 -0
  23. package/dist/chunk-XJOQ5IUT.js +110 -0
  24. package/dist/chunk-XJOQ5IUT.js.map +1 -0
  25. package/dist/cli.cjs +964 -0
  26. package/dist/cli.cjs.map +1 -0
  27. package/dist/cli.d.cts +7 -0
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.js +9 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/disclosure-5a3UIVQZ.d.cts +70 -0
  32. package/dist/disclosure-C7Y_uY73.d.ts +70 -0
  33. package/dist/elements-auto.cjs +642 -0
  34. package/dist/elements-auto.cjs.map +1 -0
  35. package/dist/elements-auto.d.cts +3 -0
  36. package/dist/elements-auto.d.ts +3 -0
  37. package/dist/elements-auto.js +16 -0
  38. package/dist/elements-auto.js.map +1 -0
  39. package/dist/elements.cjs +639 -0
  40. package/dist/elements.cjs.map +1 -0
  41. package/dist/elements.d.cts +47 -0
  42. package/dist/elements.d.ts +47 -0
  43. package/dist/elements.js +13 -0
  44. package/dist/elements.js.map +1 -0
  45. package/dist/image-B7YvdITx.d.ts +49 -0
  46. package/dist/image-BNrEO-hj.d.cts +49 -0
  47. package/dist/index.cjs +1344 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.cts +124 -0
  50. package/dist/index.d.ts +124 -0
  51. package/dist/index.js +77 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/types-WWHzrNlc.d.cts +90 -0
  54. package/dist/types-WWHzrNlc.d.ts +90 -0
  55. package/package.json +123 -3
  56. package/src/ai-sdk.ts +252 -0
  57. package/src/bin.ts +6 -0
  58. package/src/cli.ts +160 -0
  59. package/src/disclosure.ts +172 -0
  60. package/src/elements-auto.ts +5 -0
  61. package/src/elements.ts +356 -0
  62. package/src/i18n.ts +259 -0
  63. package/src/image.ts +552 -0
  64. package/src/index.ts +66 -0
  65. package/src/inflate.ts +189 -0
  66. package/src/marking.ts +237 -0
  67. package/src/types.ts +115 -0
  68. package/src/watermark.ts +165 -0
  69. package/styles.css +103 -0
package/src/inflate.ts ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * A small synchronous zlib inflater (RFC 1950/1951), enough to read compressed XMP from PNG
3
+ * zTXt and iTXt chunks in any JavaScript runtime. Based on the structure of tinf by
4
+ * Jørgen Ibsen (zlib licence).
5
+ */
6
+
7
+ interface Tree {
8
+ counts: Uint16Array;
9
+ symbols: Uint16Array;
10
+ }
11
+
12
+ const LENGTH_BASE = [
13
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
14
+ 163, 195, 227, 258,
15
+ ];
16
+ const LENGTH_EXTRA = [
17
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
18
+ ];
19
+ const DIST_BASE = [
20
+ 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049,
21
+ 3073, 4097, 6145, 8193, 12289, 16385, 24577,
22
+ ];
23
+ const DIST_EXTRA = [
24
+ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13,
25
+ ];
26
+ const CODE_LENGTH_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
27
+
28
+ function buildTree(lengths: ArrayLike<number>, offset: number, count: number): Tree {
29
+ const tree: Tree = { counts: new Uint16Array(16), symbols: new Uint16Array(count) };
30
+ for (let i = 0; i < count; i++) {
31
+ const length = lengths[offset + i] ?? 0;
32
+ tree.counts[length] = (tree.counts[length] ?? 0) + 1;
33
+ }
34
+ tree.counts[0] = 0;
35
+ const offsets = new Uint16Array(16);
36
+ let sum = 0;
37
+ for (let i = 0; i < 16; i++) {
38
+ offsets[i] = sum;
39
+ sum += tree.counts[i] ?? 0;
40
+ }
41
+ for (let i = 0; i < count; i++) {
42
+ const length = lengths[offset + i] ?? 0;
43
+ if (length) {
44
+ const at = offsets[length] ?? 0;
45
+ tree.symbols[at] = i;
46
+ offsets[length] = at + 1;
47
+ }
48
+ }
49
+ return tree;
50
+ }
51
+
52
+ let fixed: { lit: Tree; dist: Tree } | null = null;
53
+ function fixedTrees(): { lit: Tree; dist: Tree } {
54
+ if (!fixed) {
55
+ const lengths = new Uint8Array(288);
56
+ lengths.fill(8, 0, 144);
57
+ lengths.fill(9, 144, 256);
58
+ lengths.fill(7, 256, 280);
59
+ lengths.fill(8, 280, 288);
60
+ fixed = { lit: buildTree(lengths, 0, 288), dist: buildTree(new Uint8Array(30).fill(5), 0, 30) };
61
+ }
62
+ return fixed;
63
+ }
64
+
65
+ class Reader {
66
+ pos = 0;
67
+ bit = 0;
68
+ bits = 0;
69
+ constructor(readonly data: Uint8Array) {}
70
+
71
+ getBit(): number {
72
+ if (this.bits === 0) {
73
+ if (this.pos >= this.data.length) throw new Error("inflate: unexpected end of data");
74
+ this.bit = this.data[this.pos++] ?? 0;
75
+ this.bits = 8;
76
+ }
77
+ const value = this.bit & 1;
78
+ this.bit >>>= 1;
79
+ this.bits--;
80
+ return value;
81
+ }
82
+
83
+ read(count: number, base = 0): number {
84
+ let value = 0;
85
+ for (let i = 0; i < count; i++) value |= this.getBit() << i;
86
+ return value + base;
87
+ }
88
+
89
+ decode(tree: Tree): number {
90
+ let sum = 0;
91
+ let cur = 0;
92
+ let len = 0;
93
+ do {
94
+ cur = 2 * cur + this.getBit();
95
+ len++;
96
+ if (len > 15) throw new Error("inflate: invalid code");
97
+ sum += tree.counts[len] ?? 0;
98
+ cur -= tree.counts[len] ?? 0;
99
+ } while (cur >= 0);
100
+ return tree.symbols[sum + cur] ?? 0;
101
+ }
102
+ }
103
+
104
+ function dynamicTrees(r: Reader): { lit: Tree; dist: Tree } {
105
+ const hlit = r.read(5, 257);
106
+ const hdist = r.read(5, 1);
107
+ const hclen = r.read(4, 4);
108
+ const codeLengths = new Uint8Array(19);
109
+ for (let i = 0; i < hclen; i++) codeLengths[CODE_LENGTH_ORDER[i] ?? 0] = r.read(3);
110
+ const codeTree = buildTree(codeLengths, 0, 19);
111
+ const lengths = new Uint8Array(hlit + hdist);
112
+ for (let n = 0; n < hlit + hdist; ) {
113
+ const sym = r.decode(codeTree);
114
+ if (sym < 16) {
115
+ lengths[n++] = sym;
116
+ continue;
117
+ }
118
+ let repeat: number;
119
+ let value = 0;
120
+ if (sym === 16) {
121
+ if (n === 0) throw new Error("inflate: invalid repeat");
122
+ value = lengths[n - 1] ?? 0;
123
+ repeat = r.read(2, 3);
124
+ } else if (sym === 17) {
125
+ repeat = r.read(3, 3);
126
+ } else {
127
+ repeat = r.read(7, 11);
128
+ }
129
+ while (repeat-- > 0) lengths[n++] = value;
130
+ }
131
+ return { lit: buildTree(lengths, 0, hlit), dist: buildTree(lengths, hlit, hdist) };
132
+ }
133
+
134
+ /** Inflates zlib-wrapped data. Throws on corrupt input or when `maxLength` is exceeded. */
135
+ export function inflateZlib(data: Uint8Array, maxLength = 16 * 1024 * 1024): Uint8Array {
136
+ if (
137
+ data.length < 2 ||
138
+ ((data[0] ?? 0) & 0x0f) !== 8 ||
139
+ (((data[0] ?? 0) << 8) | (data[1] ?? 0)) % 31 !== 0
140
+ ) {
141
+ throw new Error("inflate: not a zlib stream");
142
+ }
143
+ const r = new Reader(data);
144
+ r.pos = 2;
145
+ let out = new Uint8Array(Math.max(1024, data.length * 4));
146
+ let length = 0;
147
+ const push = (byte: number) => {
148
+ if (length >= out.length) {
149
+ if (out.length >= maxLength) throw new Error("inflate: output too large");
150
+ const next = new Uint8Array(Math.min(maxLength, out.length * 2));
151
+ next.set(out);
152
+ out = next;
153
+ }
154
+ out[length++] = byte;
155
+ };
156
+
157
+ let final = 0;
158
+ do {
159
+ final = r.getBit();
160
+ const type = r.read(2);
161
+ if (type === 0) {
162
+ r.bits = 0;
163
+ if (r.pos + 4 > data.length) throw new Error("inflate: unexpected end of data");
164
+ const len = (data[r.pos] ?? 0) | ((data[r.pos + 1] ?? 0) << 8);
165
+ r.pos += 4;
166
+ if (r.pos + len > data.length) throw new Error("inflate: unexpected end of data");
167
+ for (let i = 0; i < len; i++) push(data[r.pos++] ?? 0);
168
+ } else if (type === 1 || type === 2) {
169
+ const { lit, dist } = type === 1 ? fixedTrees() : dynamicTrees(r);
170
+ for (;;) {
171
+ const sym = r.decode(lit);
172
+ if (sym === 256) break;
173
+ if (sym < 256) {
174
+ push(sym);
175
+ continue;
176
+ }
177
+ const index = sym - 257;
178
+ const len = r.read(LENGTH_EXTRA[index] ?? 0, LENGTH_BASE[index] ?? 0);
179
+ const distSym = r.decode(dist);
180
+ const offset = r.read(DIST_EXTRA[distSym] ?? 0, DIST_BASE[distSym] ?? 0);
181
+ if (offset > length) throw new Error("inflate: invalid distance");
182
+ for (let i = 0; i < len; i++) push(out[length - offset] ?? 0);
183
+ }
184
+ } else {
185
+ throw new Error("inflate: invalid block type");
186
+ }
187
+ } while (!final);
188
+ return out.subarray(0, length);
189
+ }
package/src/marking.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { format, getMessages } from "./i18n.js";
2
+ import type { ContentKind, LocaleOverride, Marking, MarkingInput, SourceType } from "./types.js";
3
+
4
+ const IPTC_BASE = "http://cv.iptc.org/newscodes/digitalsourcetype/";
5
+
6
+ const SCHEMA_ORG: Record<SourceType, string> = {
7
+ trainedAlgorithmicMedia: "https://schema.org/TrainedAlgorithmicMediaDigitalSource",
8
+ compositeWithTrainedAlgorithmicMedia:
9
+ "https://schema.org/CompositeWithTrainedAlgorithmicMediaDigitalSource",
10
+ algorithmicallyEnhanced: "https://schema.org/AlgorithmicallyEnhancedDigitalSource",
11
+ algorithmicMedia: "https://schema.org/AlgorithmicMediaDigitalSource",
12
+ compositeSynthetic: "https://schema.org/CompositeSyntheticDigitalSource",
13
+ };
14
+
15
+ const DEFAULT_SOURCE: Record<ContentKind, SourceType> = {
16
+ generated: "trainedAlgorithmicMedia",
17
+ edited: "compositeWithTrainedAlgorithmicMedia",
18
+ deepfake: "trainedAlgorithmicMedia",
19
+ };
20
+
21
+ /** The IPTC Digital Source Type URI, e.g. for XMP or `data-ai-source-type`. */
22
+ export function iptcSourceType(type: SourceType): string {
23
+ return IPTC_BASE + type;
24
+ }
25
+
26
+ /** The schema.org `IPTCDigitalSourceEnumeration` member for JSON-LD. */
27
+ export function schemaOrgSourceType(type: SourceType): string {
28
+ return SCHEMA_ORG[type];
29
+ }
30
+
31
+ /** Reads a source type from an IPTC or schema.org URI, or from the bare name. */
32
+ export function parseSourceType(value: string | null | undefined): SourceType | undefined {
33
+ if (!value) return undefined;
34
+ const trimmed = value.trim();
35
+ const name = trimmed.startsWith(IPTC_BASE) ? trimmed.slice(IPTC_BASE.length) : trimmed;
36
+ if (name in SCHEMA_ORG) return name as SourceType;
37
+ for (const [type, uri] of Object.entries(SCHEMA_ORG)) {
38
+ if (uri === trimmed) return type as SourceType;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ /** True for source types that mean "made or changed by a trained AI model". */
44
+ export function isAiSourceType(type: SourceType | undefined): boolean {
45
+ return (
46
+ type === "trainedAlgorithmicMedia" ||
47
+ type === "compositeWithTrainedAlgorithmicMedia" ||
48
+ type === "algorithmicallyEnhanced"
49
+ );
50
+ }
51
+
52
+ /** Applies defaults to a marking. */
53
+ export function createMarking(input: MarkingInput = {}): Marking {
54
+ const kind = input.kind ?? "generated";
55
+ const createdAt =
56
+ input.createdAt instanceof Date
57
+ ? input.createdAt.toISOString()
58
+ : (input.createdAt ?? new Date().toISOString());
59
+ const marking: Marking = {
60
+ kind,
61
+ sourceType: input.sourceType ?? DEFAULT_SOURCE[kind],
62
+ createdAt,
63
+ humanReviewed: input.humanReviewed ?? false,
64
+ };
65
+ if (input.generator) marking.generator = input.generator;
66
+ if (input.generatorVersion) marking.generatorVersion = input.generatorVersion;
67
+ if (input.provider) marking.provider = input.provider;
68
+ if (input.disclosureUrl) marking.disclosureUrl = input.disclosureUrl;
69
+ if (input.description) marking.description = input.description;
70
+ return marking;
71
+ }
72
+
73
+ /**
74
+ * `data-*` attributes that mark an element's content as AI-generated. Spread them on the
75
+ * element that wraps the content.
76
+ */
77
+ export function markingAttributes(input: Marking | MarkingInput): Record<string, string> {
78
+ const m = normalise(input);
79
+ const attributes: Record<string, string> = {
80
+ "data-ai-generated": "true",
81
+ "data-ai-kind": m.kind,
82
+ "data-ai-source-type": iptcSourceType(m.sourceType),
83
+ "data-ai-created": m.createdAt,
84
+ };
85
+ if (m.generator) attributes["data-ai-generator"] = generatorName(m);
86
+ if (m.provider) attributes["data-ai-provider"] = m.provider;
87
+ if (m.humanReviewed) attributes["data-ai-reviewed"] = "true";
88
+ return attributes;
89
+ }
90
+
91
+ /**
92
+ * Page-level `<meta>` tags for a page whose main content is AI-generated. There is no
93
+ * standardised meta tag yet, so these are a convention; pair them with {@link markingJsonLd}.
94
+ */
95
+ export function markingMetaTags(
96
+ input: Marking | MarkingInput,
97
+ ): Array<{ name: string; content: string }> {
98
+ const m = normalise(input);
99
+ const tags = [
100
+ { name: "ai-generated", content: m.kind },
101
+ { name: "ai-source-type", content: iptcSourceType(m.sourceType) },
102
+ ];
103
+ if (m.generator) tags.push({ name: "ai-generator", content: generatorName(m) });
104
+ if (m.humanReviewed) tags.push({ name: "ai-reviewed", content: "true" });
105
+ return tags;
106
+ }
107
+
108
+ /** The meta tags as an HTML string. */
109
+ export function renderMetaTags(input: Marking | MarkingInput): string {
110
+ return markingMetaTags(input)
111
+ .map((tag) => `<meta name="${escapeHtml(tag.name)}" content="${escapeHtml(tag.content)}">`)
112
+ .join("\n");
113
+ }
114
+
115
+ /** The meta tags in the shape of Next.js `metadata.other`. */
116
+ export function nextMetadata(input: Marking | MarkingInput): { other: Record<string, string> } {
117
+ return {
118
+ other: Object.fromEntries(markingMetaTags(input).map((tag) => [tag.name, tag.content])),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * schema.org JSON-LD for a creative work, with `digitalSourceType` set. Merge `extra` to add
124
+ * your own properties such as `headline` or `@type: "NewsArticle"`.
125
+ */
126
+ export function markingJsonLd(
127
+ input: Marking | MarkingInput,
128
+ extra: Record<string, unknown> = {},
129
+ ): Record<string, unknown> {
130
+ const m = normalise(input);
131
+ const data: Record<string, unknown> = {
132
+ "@context": "https://schema.org",
133
+ "@type": "CreativeWork",
134
+ digitalSourceType: schemaOrgSourceType(m.sourceType),
135
+ dateCreated: m.createdAt,
136
+ };
137
+ if (m.generator) {
138
+ data.creator = {
139
+ "@type": "SoftwareApplication",
140
+ name: m.generator,
141
+ ...(m.generatorVersion ? { softwareVersion: m.generatorVersion } : {}),
142
+ };
143
+ }
144
+ if (m.provider) data.publisher = { "@type": "Organization", name: m.provider };
145
+ if (m.description) data.description = m.description;
146
+ return { ...data, ...extra };
147
+ }
148
+
149
+ /** The JSON-LD as a `<script type="application/ld+json">` tag. */
150
+ export function renderJsonLd(
151
+ input: Marking | MarkingInput,
152
+ extra: Record<string, unknown> = {},
153
+ ): string {
154
+ const json = JSON.stringify(markingJsonLd(input, extra)).replace(/</g, "\\u003c");
155
+ return `<script type="application/ld+json">${json}</script>`;
156
+ }
157
+
158
+ export interface LabelHtmlOptions {
159
+ locale?: string | readonly string[];
160
+ messages?: LocaleOverride;
161
+ /** Where the visible label goes. Text should be labelled at the top. Default `top`. */
162
+ position?: "top" | "bottom";
163
+ /** Element that wraps label and content. Default `div`. */
164
+ tag?: "div" | "section" | "article" | "figure" | "aside";
165
+ }
166
+
167
+ /**
168
+ * Wraps server-rendered HTML in an element that carries the marking attributes and a visible
169
+ * label. Works without JavaScript; style it with `@sweberdev/witness/styles.css` or your own
170
+ * CSS for `.witness-label`.
171
+ */
172
+ export function labelHtml(
173
+ html: string,
174
+ input: Marking | MarkingInput,
175
+ options: LabelHtmlOptions = {},
176
+ ): string {
177
+ const m = normalise(input);
178
+ const tag = options.tag ?? "div";
179
+ const attributes = Object.entries(markingAttributes(m))
180
+ .map(([name, value]) => ` ${name}="${escapeHtml(value)}"`)
181
+ .join("");
182
+ const label = renderLabelLine(m, options);
183
+ const body = options.position === "bottom" ? `${html}${label}` : `${label}${html}`;
184
+ return `<${tag} class="witness-content"${attributes}>${body}</${tag}>`;
185
+ }
186
+
187
+ /** One line of visible label text, e.g. "KI-generiert · Erzeugt mit Claude". */
188
+ export function labelText(
189
+ input: Marking | MarkingInput,
190
+ options: Pick<LabelHtmlOptions, "locale" | "messages"> = {},
191
+ ): string {
192
+ const m = normalise(input);
193
+ const messages = getMessages(options.locale, options.messages);
194
+ const parts = [messages.kinds[m.kind].label];
195
+ if (m.generator) {
196
+ parts.push(format(messages.ui.generatedWith, { generator: generatorName(m) }));
197
+ }
198
+ if (m.humanReviewed) parts.push(messages.ui.humanReviewed);
199
+ return parts.join(" · ");
200
+ }
201
+
202
+ function renderLabelLine(m: Marking, options: LabelHtmlOptions): string {
203
+ const messages = getMessages(options.locale, options.messages);
204
+ const text = escapeHtml(labelText(m, options));
205
+ const link = m.disclosureUrl
206
+ ? ` <a class="witness-label__link" href="${escapeHtml(m.disclosureUrl)}">${escapeHtml(messages.ui.moreInfo)}</a>`
207
+ : "";
208
+ return `<p class="witness-label" role="note"><span class="witness-label__icon" aria-hidden="true">AI</span> <span class="witness-label__text">${text}</span>${link}</p>`;
209
+ }
210
+
211
+ /** "Model 1.2" when a version is known. */
212
+ export function generatorName(m: Pick<Marking, "generator" | "generatorVersion">): string {
213
+ if (!m.generator) return "";
214
+ return m.generatorVersion ? `${m.generator} ${m.generatorVersion}` : m.generator;
215
+ }
216
+
217
+ function normalise(input: Marking | MarkingInput): Marking {
218
+ return isMarking(input) ? input : createMarking(input);
219
+ }
220
+
221
+ function isMarking(input: Marking | MarkingInput): input is Marking {
222
+ return (
223
+ typeof input.kind === "string" &&
224
+ typeof input.sourceType === "string" &&
225
+ typeof input.createdAt === "string" &&
226
+ typeof input.humanReviewed === "boolean"
227
+ );
228
+ }
229
+
230
+ export function escapeHtml(value: string): string {
231
+ return value
232
+ .replace(/&/g, "&amp;")
233
+ .replace(/</g, "&lt;")
234
+ .replace(/>/g, "&gt;")
235
+ .replace(/"/g, "&quot;")
236
+ .replace(/'/g, "&#39;");
237
+ }
package/src/types.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The transparency situations Article 50 of the EU AI Act describes.
3
+ *
4
+ * - `chatbot`: a person interacts directly with an AI system (Art. 50(1)).
5
+ * - `generated`: image, audio, video or text produced by an AI system (Art. 50(2) and 50(4)).
6
+ * - `edited`: existing content altered with an AI system (Art. 50(2) and 50(4)).
7
+ * - `deepfake`: generated or manipulated content that resembles real people, objects,
8
+ * places or events and could falsely appear authentic (Art. 50(4)).
9
+ * - `emotion-recognition` and `biometric-categorisation`: people are exposed to such a
10
+ * system (Art. 50(3)).
11
+ */
12
+ export type DisclosureKind =
13
+ | "chatbot"
14
+ | "generated"
15
+ | "edited"
16
+ | "deepfake"
17
+ | "emotion-recognition"
18
+ | "biometric-categorisation";
19
+
20
+ export const DISCLOSURE_KINDS: readonly DisclosureKind[] = [
21
+ "chatbot",
22
+ "generated",
23
+ "edited",
24
+ "deepfake",
25
+ "emotion-recognition",
26
+ "biometric-categorisation",
27
+ ];
28
+
29
+ /** The kinds that describe a piece of content and can be written into a marking. */
30
+ export type ContentKind = "generated" | "edited" | "deepfake";
31
+
32
+ /**
33
+ * IPTC Digital Source Type values relevant to AI. See
34
+ * https://cv.iptc.org/newscodes/digitalsourcetype/
35
+ */
36
+ export type SourceType =
37
+ | "trainedAlgorithmicMedia"
38
+ | "compositeWithTrainedAlgorithmicMedia"
39
+ | "algorithmicallyEnhanced"
40
+ | "algorithmicMedia"
41
+ | "compositeSynthetic";
42
+
43
+ /** Wording for one disclosure kind in one language. */
44
+ export interface DisclosureText {
45
+ /** Two or three words for a badge, e.g. "AI-generated". */
46
+ label: string;
47
+ /** One line for the heading of a notice. */
48
+ title: string;
49
+ /** One or two plain sentences that explain the label. */
50
+ body: string;
51
+ }
52
+
53
+ /** Interface strings shared by the notice and label components. */
54
+ export interface UiText {
55
+ /** Button that collapses the chatbot notice. */
56
+ acknowledge: string;
57
+ /** Accessible name of the button that opens label details. */
58
+ details: string;
59
+ /** "Generated with {generator}". `{generator}` is replaced. */
60
+ generatedWith: string;
61
+ /** "Created on {date}". `{date}` is replaced. */
62
+ createdOn: string;
63
+ /** Shown when a person reviewed the content and took editorial responsibility. */
64
+ humanReviewed: string;
65
+ /** Link text to a page with more information. */
66
+ moreInfo: string;
67
+ /** Closes the details popover. */
68
+ close: string;
69
+ }
70
+
71
+ export interface LocaleMessages {
72
+ kinds: Record<DisclosureKind, DisclosureText>;
73
+ ui: UiText;
74
+ }
75
+
76
+ /** A partial override of a locale, e.g. to switch German to the informal "du". */
77
+ export interface LocaleOverride {
78
+ kinds?: Partial<Record<DisclosureKind, Partial<DisclosureText>>>;
79
+ ui?: Partial<UiText>;
80
+ }
81
+
82
+ /** What a marking says about a piece of content. Input for {@link createMarking}. */
83
+ export interface MarkingInput {
84
+ /** Defaults to `generated`. */
85
+ kind?: ContentKind;
86
+ /** Defaults from `kind`: `trainedAlgorithmicMedia`, or the composite type for `edited`. */
87
+ sourceType?: SourceType;
88
+ /** Name of the AI system or model, e.g. "gpt-image-2" or "Claude". */
89
+ generator?: string;
90
+ /** Version of the AI system, if known. */
91
+ generatorVersion?: string;
92
+ /** Who provides or operates the AI system, e.g. your company. */
93
+ provider?: string;
94
+ /** ISO 8601 timestamp. Defaults to now. */
95
+ createdAt?: string | Date;
96
+ /** A page that explains how and why you use AI. */
97
+ disclosureUrl?: string;
98
+ /** A person reviewed the content and holds editorial responsibility (Art. 50(4), text). */
99
+ humanReviewed?: boolean;
100
+ /** Free text, e.g. "Image generated from the prompt by the newsroom". */
101
+ description?: string;
102
+ }
103
+
104
+ /** A normalised marking with all defaults applied. */
105
+ export interface Marking {
106
+ kind: ContentKind;
107
+ sourceType: SourceType;
108
+ generator?: string;
109
+ generatorVersion?: string;
110
+ provider?: string;
111
+ createdAt: string;
112
+ disclosureUrl?: string;
113
+ humanReviewed: boolean;
114
+ description?: string;
115
+ }