@sweberdev/witness 0.1.0 → 0.2.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.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { D as Disclosure, a as DisclosureEvent, b as DisclosureOptions, c as DisclosureStorage, d as createDisclosure, m as memoryStorage } from './disclosure-C7Y_uY73.js';
2
2
  import { D as DisclosureKind, L as LocaleOverride, b as DisclosureText, c as LocaleMessages, a as MarkingInput, M as Marking, S as SourceType } from './types-WWHzrNlc.js';
3
3
  export { C as ContentKind, d as DISCLOSURE_KINDS, U as UiText } from './types-WWHzrNlc.js';
4
- export { I as ImageFormat, a as ImageMarkingInfo, M as MarkImageOptions, b as MarkImageResult, c as buildXmp, d as detectImageFormat, m as markImage, r as readImageMarking } from './image-B7YvdITx.js';
4
+ import { I as ImageFormat, M as MarkImageOptions, a as ImageMarkingInfo } from './image-B7YvdITx.js';
5
+ export { b as MarkImageResult, c as buildXmp, d as detectImageFormat, m as markImage, r as readImageMarking } from './image-B7YvdITx.js';
5
6
 
6
7
  /** The locales bundled with the free package. */
7
8
  declare const BUILT_IN_LOCALES: readonly ["en", "de", "fr", "it"];
@@ -80,6 +81,48 @@ declare function labelText(input: Marking | MarkingInput, options?: Pick<LabelHt
80
81
  declare function generatorName(m: Pick<Marking, "generator" | "generatorVersion">): string;
81
82
  declare function escapeHtml(value: string): string;
82
83
 
84
+ /**
85
+ * Machine-readable AI marking for audio and video: the same XMP packet as for images, with the
86
+ * IPTC Digital Source Type, written where the XMP specification puts it for each container.
87
+ *
88
+ * - MP3: an ID3v2 `PRIV` frame with the owner `XMP`.
89
+ * - WAV: a RIFF `_PMX` chunk.
90
+ * - MP4, MOV and M4A: a top-level `uuid` box with the XMP UUID, appended at the end of the file
91
+ * so no sample offsets move.
92
+ *
93
+ * The audio and video data is not touched. Files that carry a C2PA manifest are left unchanged
94
+ * by default, as for images.
95
+ */
96
+
97
+ type MediaFormat = "mp3" | "wav" | "mp4";
98
+ type MarkMediaOptions = MarkImageOptions;
99
+ interface MarkMediaResult {
100
+ bytes: Uint8Array;
101
+ status: "marked" | "skipped-c2pa" | "unsupported";
102
+ format: MediaFormat | null;
103
+ }
104
+ interface MediaMarkingInfo extends Omit<ImageMarkingInfo, "format"> {
105
+ format: MediaFormat | null;
106
+ }
107
+ interface MarkFileResult {
108
+ bytes: Uint8Array;
109
+ status: "marked" | "skipped-c2pa" | "unsupported";
110
+ format: ImageFormat | MediaFormat | null;
111
+ }
112
+ interface MarkingInfo extends Omit<ImageMarkingInfo, "format"> {
113
+ format: ImageFormat | MediaFormat | null;
114
+ }
115
+ /** Detects MP3, WAV or an ISO media file (MP4, MOV, M4A) from the first bytes. */
116
+ declare function detectMediaFormat(bytes: Uint8Array): MediaFormat | null;
117
+ /** Writes the marking as XMP into an MP3, WAV, MP4, MOV or M4A file. */
118
+ declare function markMedia(bytes: Uint8Array, input?: Marking | MarkingInput, options?: MarkMediaOptions): MarkMediaResult;
119
+ /** Reads the AI marking of an audio or video file, whoever wrote it. */
120
+ declare function readMediaMarking(bytes: Uint8Array): MediaMarkingInfo;
121
+ /** Marks an image, audio or video file, whichever it is. */
122
+ declare function markFile(bytes: Uint8Array, input?: Marking | MarkingInput, options?: MarkMediaOptions): MarkFileResult;
123
+ /** Reads the AI marking of an image, audio or video file. */
124
+ declare function readMarking(bytes: Uint8Array): MarkingInfo;
125
+
83
126
  /**
84
127
  * An invisible, machine-readable marker for AI-generated text.
85
128
  *
@@ -121,4 +164,4 @@ declare function hasTextWatermark(text: string): boolean;
121
164
  */
122
165
  declare function stripTextWatermark(text: string): string;
123
166
 
124
- export { BUILT_IN_LOCALES, DisclosureKind, DisclosureText, type LabelHtmlOptions, LocaleMessages, LocaleOverride, Marking, MarkingInput, type ReadTextWatermark, SourceType, type TextWatermark, createMarking, disclosureText, escapeHtml, format, generatorName, getMessages, hasTextWatermark, iptcSourceType, isAiSourceType, labelHtml, labelText, markingAttributes, markingJsonLd, markingMetaTags, nextMetadata, parseSourceType, readTextWatermark, registerLocale, registeredLocales, renderJsonLd, renderMetaTags, resolveLocale, schemaOrgSourceType, stripTextWatermark, watermarkSuffix, watermarkText };
167
+ export { BUILT_IN_LOCALES, DisclosureKind, DisclosureText, ImageFormat, ImageMarkingInfo, type LabelHtmlOptions, LocaleMessages, LocaleOverride, type MarkFileResult, MarkImageOptions, type MarkMediaOptions, type MarkMediaResult, Marking, type MarkingInfo, MarkingInput, type MediaFormat, type MediaMarkingInfo, type ReadTextWatermark, SourceType, type TextWatermark, createMarking, detectMediaFormat, disclosureText, escapeHtml, format, generatorName, getMessages, hasTextWatermark, iptcSourceType, isAiSourceType, labelHtml, labelText, markFile, markMedia, markingAttributes, markingJsonLd, markingMetaTags, nextMetadata, parseSourceType, readMarking, readMediaMarking, readTextWatermark, registerLocale, registeredLocales, renderJsonLd, renderMetaTags, resolveLocale, schemaOrgSourceType, stripTextWatermark, watermarkSuffix, watermarkText };
package/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ detectMediaFormat,
3
+ markFile,
4
+ markMedia,
5
+ readMarking,
6
+ readMediaMarking
7
+ } from "./chunk-F7OWVTCS.js";
1
8
  import {
2
9
  buildXmp,
3
10
  createMarking,
@@ -23,7 +30,7 @@ import {
23
30
  stripTextWatermark,
24
31
  watermarkSuffix,
25
32
  watermarkText
26
- } from "./chunk-APXU3MTX.js";
33
+ } from "./chunk-GXMGYIOK.js";
27
34
  import {
28
35
  DISCLOSURE_KINDS,
29
36
  createDisclosure,
@@ -45,6 +52,7 @@ export {
45
52
  createDisclosure,
46
53
  createMarking,
47
54
  detectImageFormat,
55
+ detectMediaFormat,
48
56
  disclosureText,
49
57
  escapeHtml,
50
58
  format,
@@ -55,7 +63,9 @@ export {
55
63
  isAiSourceType,
56
64
  labelHtml,
57
65
  labelText,
66
+ markFile,
58
67
  markImage,
68
+ markMedia,
59
69
  markingAttributes,
60
70
  markingJsonLd,
61
71
  markingMetaTags,
@@ -63,6 +73,8 @@ export {
63
73
  nextMetadata,
64
74
  parseSourceType,
65
75
  readImageMarking,
76
+ readMarking,
77
+ readMediaMarking,
66
78
  readTextWatermark,
67
79
  registerLocale,
68
80
  registeredLocales,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sweberdev/witness",
3
- "version": "0.1.0",
4
- "description": "AI transparency kit for Article 50 of the EU AI Act: chatbot notices, AI labels in DE/EN/FR/IT, IPTC/XMP marking for images, an invisible text watermark and Vercel AI SDK middleware.",
3
+ "version": "0.2.0",
4
+ "description": "AI transparency kit for Article 50 of the EU AI Act: chatbot notices, AI labels in DE/EN/FR/IT, IPTC/XMP marking for images, audio and video, an invisible text watermark and Vercel AI SDK middleware.",
5
5
  "license": "MIT",
6
6
  "author": "Seya Weber (https://github.com/sxwxbxr)",
7
7
  "homepage": "https://packages.sweber.dev/witness/docs",
package/src/cli.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { basename, extname, join } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
- import { detectImageFormat, markImage, readImageMarking } from "./image.js";
4
+ import { detectImageFormat } from "./image.js";
5
5
  import { iptcSourceType } from "./marking.js";
6
+ import { detectMediaFormat, markFile, readMarking } from "./media.js";
6
7
  import type { ContentKind, MarkingInput } from "./types.js";
7
8
  import { readTextWatermark } from "./watermark.js";
8
9
 
@@ -11,7 +12,7 @@ export interface CliIo {
11
12
  err: (line: string) => void;
12
13
  }
13
14
 
14
- const HELP = `witness: AI Act Article 50 marking for images and text
15
+ const HELP = `witness: AI Act Article 50 marking for images, audio, video and text
15
16
 
16
17
  Usage
17
18
  witness mark <files...> (--out <dir> | --in-place) [options]
@@ -30,7 +31,8 @@ inspect options
30
31
  --json one JSON object per file
31
32
  --require exit 1 if a file carries no AI marking
32
33
 
33
- Images: PNG, JPEG, WebP (XMP with IPTC Digital Source Type).
34
+ Images: PNG, JPEG, WebP. Audio and video: MP3, WAV, MP4, MOV, M4A.
35
+ All get XMP with the IPTC Digital Source Type.
34
36
  Text files are checked for the Witness text watermark.`;
35
37
 
36
38
  const KINDS: ContentKind[] = ["generated", "edited", "deepfake"];
@@ -86,11 +88,11 @@ async function mark(argv: string[], io: CliIo): Promise<number> {
86
88
  let failed = 0;
87
89
  for (const file of positionals) {
88
90
  const bytes = new Uint8Array(await readFile(file));
89
- const result = markImage(bytes, input, {
91
+ const result = markFile(bytes, input, {
90
92
  c2pa: values["overwrite-c2pa"] ? "overwrite" : "skip",
91
93
  });
92
94
  if (result.status === "unsupported") {
93
- io.err(`${file}: not a PNG, JPEG or WebP file, skipped`);
95
+ io.err(`${file}: not a PNG, JPEG, WebP, MP3, WAV or MP4 file, skipped`);
94
96
  failed++;
95
97
  continue;
96
98
  }
@@ -115,9 +117,9 @@ async function inspect(argv: string[], io: CliIo): Promise<number> {
115
117
  let unmarked = 0;
116
118
  for (const file of positionals) {
117
119
  const bytes = new Uint8Array(await readFile(file));
118
- const format = detectImageFormat(bytes);
120
+ const format = detectImageFormat(bytes) ?? detectMediaFormat(bytes);
119
121
  if (format) {
120
- const info = readImageMarking(bytes);
122
+ const info = readMarking(bytes);
121
123
  const marked = info.aiGenerated || info.c2pa;
122
124
  if (!marked) unmarked++;
123
125
  if (values.json) {
package/src/elements.ts CHANGED
@@ -103,6 +103,8 @@ export class WitnessNoticeElement extends BaseElement {
103
103
  #disclosure: Disclosure | null = null;
104
104
  #unsubscribe: (() => void) | null = null;
105
105
  #expanded = false;
106
+ /** `id@version` of the disclosure a `shown` event was sent for, so attribute changes don't repeat it. */
107
+ #shownFor: string | null = null;
106
108
 
107
109
  connectedCallback(): void {
108
110
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
@@ -114,7 +116,8 @@ export class WitnessNoticeElement extends BaseElement {
114
116
  this.#unsubscribe = null;
115
117
  }
116
118
 
117
- attributeChangedCallback(): void {
119
+ attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void {
120
+ if (oldValue === newValue) return;
118
121
  if (this.isConnected && this.shadowRoot) this.#setup();
119
122
  }
120
123
 
@@ -189,7 +192,11 @@ export class WitnessNoticeElement extends BaseElement {
189
192
  ),
190
193
  );
191
194
  root.replaceChildren(style, box);
192
- disclosure.markShown();
195
+ const shownFor = `${disclosure.id}@${disclosure.version}`;
196
+ if (this.#shownFor !== shownFor) {
197
+ this.#shownFor = shownFor;
198
+ disclosure.markShown();
199
+ }
193
200
  } else {
194
201
  const reopen = h(
195
202
  doc,
package/src/image.ts CHANGED
@@ -50,7 +50,8 @@ export interface ImageMarkingInfo {
50
50
  }
51
51
 
52
52
  const XMP_NS = "http://ns.adobe.com/xap/1.0/\0";
53
- const WITNESS_NS = "https://packages.sweber.dev/witness/ns/1.0/";
53
+ /** @internal */
54
+ export const WITNESS_NS = "https://packages.sweber.dev/witness/ns/1.0/";
54
55
 
55
56
  /** Detects PNG, JPEG or WebP from the first bytes. */
56
57
  export function detectImageFormat(bytes: Uint8Array): ImageFormat | null {
@@ -165,8 +166,8 @@ function buildDescription(m: Marking): string {
165
166
  : `<rdf:Description${attrs}/>`;
166
167
  }
167
168
 
168
- /** Reads a simple property in attribute or element form. */
169
- function xmpProperty(xmp: string, name: string): string | undefined {
169
+ /** Reads a simple property in attribute or element form. @internal */
170
+ export function xmpProperty(xmp: string, name: string): string | undefined {
170
171
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
171
172
  const attribute = new RegExp(`${escaped}="([^"]*)"`).exec(xmp);
172
173
  if (attribute?.[1] !== undefined) return unescapeXml(attribute[1]);
package/src/index.ts CHANGED
@@ -43,6 +43,19 @@ export {
43
43
  renderMetaTags,
44
44
  schemaOrgSourceType,
45
45
  } from "./marking.js";
46
+ export {
47
+ detectMediaFormat,
48
+ type MarkFileResult,
49
+ type MarkingInfo,
50
+ type MarkMediaOptions,
51
+ type MarkMediaResult,
52
+ type MediaFormat,
53
+ type MediaMarkingInfo,
54
+ markFile,
55
+ markMedia,
56
+ readMarking,
57
+ readMediaMarking,
58
+ } from "./media.js";
46
59
  export {
47
60
  type ContentKind,
48
61
  DISCLOSURE_KINDS,