@affectively/synthetic-watermark 1.0.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/index.d.mts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +364 -0
- package/dist/index.mjs +327 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-01-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release
|
|
10
|
+
- PNG image watermarking using iTXt chunks
|
|
11
|
+
- MP3 audio watermarking using ID3v2 COMM frames
|
|
12
|
+
- Watermark detection for both formats
|
|
13
|
+
- Convenience functions for common AI sources (DALL-E, Gemini, etc.)
|
|
14
|
+
- Full TypeScript support
|
|
15
|
+
- Zero external dependencies
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AFFECTIVELY
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @affectively/synthetic-watermark
|
|
2
|
+
|
|
3
|
+
Invisible watermarking for AI-generated content. Embeds synthetic origin markers into PNG images and MP3 audio files for authenticity verification and AI safety compliance.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Image Watermarking** - Embeds invisible metadata into PNG files using iTXt chunks
|
|
8
|
+
- **Audio Watermarking** - Embeds metadata into MP3 files using ID3v2 tags
|
|
9
|
+
- **Detection** - Verify if content has synthetic origin markers
|
|
10
|
+
- **Zero Dependencies** - Uses only Node.js Buffer API
|
|
11
|
+
- **Non-Destructive** - Watermarks are stored in metadata, not visible/audible content
|
|
12
|
+
- **AI Safety** - Helps identify AI-generated content for authenticity verification
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @affectively/synthetic-watermark
|
|
18
|
+
# or
|
|
19
|
+
bun add @affectively/synthetic-watermark
|
|
20
|
+
# or
|
|
21
|
+
yarn add @affectively/synthetic-watermark
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Image Watermarking
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import {
|
|
30
|
+
embedImageWatermark,
|
|
31
|
+
detectSyntheticImageWatermark,
|
|
32
|
+
} from '@affectively/synthetic-watermark';
|
|
33
|
+
|
|
34
|
+
// Watermark an AI-generated image
|
|
35
|
+
const pngBuffer = await fs.readFile('ai-generated-image.png');
|
|
36
|
+
const watermarkedImage = embedImageWatermark(pngBuffer, 'png', {
|
|
37
|
+
source: 'dalle',
|
|
38
|
+
model: 'dall-e-3',
|
|
39
|
+
platform: 'MyApp',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Save the watermarked image
|
|
43
|
+
await fs.writeFile('watermarked-image.png', watermarkedImage);
|
|
44
|
+
|
|
45
|
+
// Detect watermark
|
|
46
|
+
const detected = detectSyntheticImageWatermark(watermarkedImage);
|
|
47
|
+
if (detected) {
|
|
48
|
+
console.log('Synthetic origin detected:', detected);
|
|
49
|
+
// { platform: 'MyApp', source: 'dalle', model: 'dall-e-3', timestamp: 1234567890 }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Audio Watermarking
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import {
|
|
57
|
+
embedAudioWatermark,
|
|
58
|
+
detectSyntheticWatermark,
|
|
59
|
+
} from '@affectively/synthetic-watermark';
|
|
60
|
+
|
|
61
|
+
// Watermark AI-generated audio
|
|
62
|
+
const mp3Buffer = await fs.readFile('tts-audio.mp3');
|
|
63
|
+
const watermarkedAudio = embedAudioWatermark(mp3Buffer, 'mp3', {
|
|
64
|
+
source: 'tts',
|
|
65
|
+
platform: 'MyApp',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Save the watermarked audio
|
|
69
|
+
await fs.writeFile('watermarked-audio.mp3', watermarkedAudio);
|
|
70
|
+
|
|
71
|
+
// Detect watermark
|
|
72
|
+
const detected = detectSyntheticWatermark(watermarkedAudio);
|
|
73
|
+
if (detected) {
|
|
74
|
+
console.log('Synthetic audio detected:', detected);
|
|
75
|
+
// { platform: 'MyApp', source: 'tts', timestamp: 1234567890 }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Convenience Functions
|
|
80
|
+
|
|
81
|
+
### For Images
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import {
|
|
85
|
+
watermarkDallEImage,
|
|
86
|
+
watermarkGeminiImage,
|
|
87
|
+
watermarkDigitalTwinImage,
|
|
88
|
+
watermarkCyranoImage,
|
|
89
|
+
} from '@affectively/synthetic-watermark';
|
|
90
|
+
|
|
91
|
+
// Pre-configured for specific AI image generators
|
|
92
|
+
const watermarked = watermarkDallEImage(imageBuffer, 'png', optionalUserIdHash);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### For Audio
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import {
|
|
99
|
+
watermarkDigitalTwinAudio,
|
|
100
|
+
watermarkHologramAudio,
|
|
101
|
+
watermarkCyranoAudio,
|
|
102
|
+
} from '@affectively/synthetic-watermark';
|
|
103
|
+
|
|
104
|
+
// Pre-configured for specific TTS systems
|
|
105
|
+
const watermarked = watermarkCyranoAudio(audioBuffer, 'mp3');
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Watermark Format
|
|
109
|
+
|
|
110
|
+
### Image Watermark (PNG iTXt chunk)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
SyntheticOrigin: SYNTHETIC_IMAGE|platform|source|timestamp|userIdHash|model
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Audio Watermark (ID3v2 COMM frame)
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
SYNTHETIC_ORIGIN: SYNTHETIC_AUDIO|platform|source|timestamp|userIdHash
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
### Image Functions
|
|
125
|
+
|
|
126
|
+
#### `embedImageWatermark(buffer, format, config?)`
|
|
127
|
+
|
|
128
|
+
Embeds an invisible watermark into a PNG image.
|
|
129
|
+
|
|
130
|
+
- `buffer` - Image data (Uint8Array)
|
|
131
|
+
- `format` - Image format (currently only 'png' supported)
|
|
132
|
+
- `config` - Optional configuration:
|
|
133
|
+
- `platform` - Platform identifier (default: 'SYNTHETIC')
|
|
134
|
+
- `source` - Source system (e.g., 'dalle', 'gemini')
|
|
135
|
+
- `model` - Model name (e.g., 'dall-e-3')
|
|
136
|
+
- `userIdHash` - Hashed user ID for audit trail
|
|
137
|
+
|
|
138
|
+
Returns: Watermarked image buffer
|
|
139
|
+
|
|
140
|
+
#### `detectSyntheticImageWatermark(buffer)`
|
|
141
|
+
|
|
142
|
+
Detects and extracts watermark from PNG image.
|
|
143
|
+
|
|
144
|
+
Returns: `ImageWatermarkConfig | null`
|
|
145
|
+
|
|
146
|
+
### Audio Functions
|
|
147
|
+
|
|
148
|
+
#### `embedAudioWatermark(buffer, format, config?)`
|
|
149
|
+
|
|
150
|
+
Embeds an invisible watermark into MP3 audio.
|
|
151
|
+
|
|
152
|
+
- `buffer` - Audio data (Buffer)
|
|
153
|
+
- `format` - Audio format (currently only 'mp3' supported)
|
|
154
|
+
- `config` - Optional configuration:
|
|
155
|
+
- `platform` - Platform identifier (default: 'SYNTHETIC')
|
|
156
|
+
- `source` - Source system (e.g., 'tts', 'hologram')
|
|
157
|
+
- `userIdHash` - Hashed user ID for audit trail
|
|
158
|
+
|
|
159
|
+
Returns: Watermarked audio buffer
|
|
160
|
+
|
|
161
|
+
#### `detectSyntheticWatermark(buffer)`
|
|
162
|
+
|
|
163
|
+
Detects and extracts watermark from MP3 audio.
|
|
164
|
+
|
|
165
|
+
Returns: `WatermarkConfig | null`
|
|
166
|
+
|
|
167
|
+
## Use Cases
|
|
168
|
+
|
|
169
|
+
1. **AI Safety Compliance** - Mark AI-generated content for authenticity verification
|
|
170
|
+
2. **Content Authentication** - Verify the origin of synthetic media
|
|
171
|
+
3. **Audit Trail** - Track when and by whom content was generated
|
|
172
|
+
4. **Platform Trust** - Help users identify AI-generated content
|
|
173
|
+
5. **Regulatory Compliance** - Support emerging AI content disclosure requirements
|
|
174
|
+
|
|
175
|
+
## Technical Details
|
|
176
|
+
|
|
177
|
+
### PNG Watermarking
|
|
178
|
+
|
|
179
|
+
- Uses iTXt (International Text) chunk per PNG specification
|
|
180
|
+
- Inserted before IEND chunk
|
|
181
|
+
- Includes proper CRC32 checksum
|
|
182
|
+
- Non-destructive to image data
|
|
183
|
+
|
|
184
|
+
### MP3 Watermarking
|
|
185
|
+
|
|
186
|
+
- Uses ID3v2.3 COMM (Comment) frame
|
|
187
|
+
- Replaces existing ID3 tag if present
|
|
188
|
+
- Syncsafe integer encoding for size
|
|
189
|
+
- Compatible with standard ID3 readers
|
|
190
|
+
|
|
191
|
+
## Browser Support
|
|
192
|
+
|
|
193
|
+
Works in Node.js and browser environments (requires Buffer polyfill in browser).
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT © AFFECTIVELY
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Image Watermarking Service
|
|
5
|
+
*
|
|
6
|
+
* Embeds INVISIBLE metadata into generated images to indicate
|
|
7
|
+
* "Synthetic Origin" - marking AI-generated images for safety/authenticity.
|
|
8
|
+
*
|
|
9
|
+
* These watermarks are completely invisible to users.
|
|
10
|
+
* They are stored in the PNG file's metadata chunks (iTXt/tEXt),
|
|
11
|
+
* not rendered on the image itself. Similar to EXIF data in photos.
|
|
12
|
+
*
|
|
13
|
+
* For PNG format: Uses ancillary iTXt chunk (international text)
|
|
14
|
+
* For JPEG format: Could use EXIF/XMP metadata (future)
|
|
15
|
+
* For WebP format: Could use XMP metadata (future)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Watermark configuration
|
|
20
|
+
*/
|
|
21
|
+
interface ImageWatermarkConfig {
|
|
22
|
+
/** Platform identifier */
|
|
23
|
+
platform: string;
|
|
24
|
+
/** Timestamp of generation */
|
|
25
|
+
timestamp: number;
|
|
26
|
+
/** Source system (e.g., 'dalle', 'imagen', 'gemini', 'digital_twin') */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Optional user ID (hashed) for audit trail */
|
|
29
|
+
userIdHash?: string;
|
|
30
|
+
/** Optional model name */
|
|
31
|
+
model?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Embed INVISIBLE watermark into image buffer
|
|
35
|
+
*
|
|
36
|
+
* For PNG: Inserts iTXt chunk before IEND
|
|
37
|
+
* For other formats: Passes through unchanged (future: EXIF/XMP)
|
|
38
|
+
*
|
|
39
|
+
* The watermark is stored in metadata, NOT visible on the image.
|
|
40
|
+
*
|
|
41
|
+
* @param imageBuffer - Raw image data
|
|
42
|
+
* @param format - Image format (png, jpeg, webp)
|
|
43
|
+
* @param config - Watermark configuration
|
|
44
|
+
* @returns Watermarked image buffer
|
|
45
|
+
*/
|
|
46
|
+
declare function embedImageWatermark(imageBuffer: Uint8Array, format?: string, config?: Partial<ImageWatermarkConfig>): Buffer;
|
|
47
|
+
/**
|
|
48
|
+
* Detect synthetic origin watermark from image
|
|
49
|
+
*
|
|
50
|
+
* @param imageBuffer - Image data to check
|
|
51
|
+
* @returns Watermark info if found, null otherwise
|
|
52
|
+
*/
|
|
53
|
+
declare function detectSyntheticImageWatermark(imageBuffer: Buffer): ImageWatermarkConfig | null;
|
|
54
|
+
/**
|
|
55
|
+
* Convenience function for DALL-E generated images
|
|
56
|
+
*/
|
|
57
|
+
declare function watermarkDallEImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
58
|
+
/**
|
|
59
|
+
* Convenience function for Gemini Imagen images
|
|
60
|
+
*/
|
|
61
|
+
declare function watermarkGeminiImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
62
|
+
/**
|
|
63
|
+
* Convenience function for Digital Twin generated images
|
|
64
|
+
*/
|
|
65
|
+
declare function watermarkDigitalTwinImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
66
|
+
/**
|
|
67
|
+
* Convenience function for Cyrano-generated images
|
|
68
|
+
*/
|
|
69
|
+
declare function watermarkCyranoImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Audio Watermarking Service
|
|
73
|
+
*
|
|
74
|
+
* Embeds invisible/inaudible metadata into synthesized audio to indicate
|
|
75
|
+
* "Synthetic Origin" - marking AI-generated audio for safety and authenticity.
|
|
76
|
+
*
|
|
77
|
+
* Approach: Adds a metadata comment to the MP3 ID3 tag or embeds a
|
|
78
|
+
* high-frequency inaudible tone pattern (above 18kHz) that serves as
|
|
79
|
+
* a digital fingerprint.
|
|
80
|
+
*
|
|
81
|
+
* For MP3 format: Uses ID3 tag metadata injection
|
|
82
|
+
* For raw formats: Could use frequency-domain watermarking (future)
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Watermark configuration
|
|
87
|
+
*/
|
|
88
|
+
interface WatermarkConfig {
|
|
89
|
+
/** Platform identifier */
|
|
90
|
+
platform: string;
|
|
91
|
+
/** Timestamp of generation */
|
|
92
|
+
timestamp: number;
|
|
93
|
+
/** Source system (e.g., 'cyrano', 'hologram', 'twin') */
|
|
94
|
+
source: string;
|
|
95
|
+
/** Optional user ID (hashed) for audit trail */
|
|
96
|
+
userIdHash?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Embed watermark into audio buffer
|
|
100
|
+
*
|
|
101
|
+
* For MP3: Prepends ID3v2 tag with synthetic origin metadata
|
|
102
|
+
* For other formats: Passes through unchanged (future: frequency watermarking)
|
|
103
|
+
*
|
|
104
|
+
* @param audioBuffer - Raw audio data
|
|
105
|
+
* @param format - Audio format (mp3, wav, etc.)
|
|
106
|
+
* @param config - Watermark configuration
|
|
107
|
+
* @returns Watermarked audio buffer
|
|
108
|
+
*/
|
|
109
|
+
declare function embedAudioWatermark(audioBuffer: Buffer, format?: string, config?: Partial<WatermarkConfig>): Buffer;
|
|
110
|
+
/**
|
|
111
|
+
* Check if audio has synthetic origin watermark
|
|
112
|
+
*
|
|
113
|
+
* @param audioBuffer - Audio data to check
|
|
114
|
+
* @returns Watermark info if found, null otherwise
|
|
115
|
+
*/
|
|
116
|
+
declare function detectSyntheticWatermark(audioBuffer: Buffer): WatermarkConfig | null;
|
|
117
|
+
/**
|
|
118
|
+
* Convenience function for Digital Twin audio
|
|
119
|
+
*/
|
|
120
|
+
declare function watermarkDigitalTwinAudio(audioBuffer: Buffer, format?: string, userIdHash?: string): Buffer;
|
|
121
|
+
/**
|
|
122
|
+
* Convenience function for Hologram audio
|
|
123
|
+
*/
|
|
124
|
+
declare function watermarkHologramAudio(audioBuffer: Buffer, format?: string): Buffer;
|
|
125
|
+
/**
|
|
126
|
+
* Convenience function for Cyrano TTS audio
|
|
127
|
+
*/
|
|
128
|
+
declare function watermarkCyranoAudio(audioBuffer: Buffer, format?: string): Buffer;
|
|
129
|
+
|
|
130
|
+
export { type ImageWatermarkConfig, type WatermarkConfig, detectSyntheticImageWatermark, detectSyntheticWatermark, embedAudioWatermark, embedImageWatermark, watermarkCyranoAudio, watermarkCyranoImage, watermarkDallEImage, watermarkDigitalTwinAudio, watermarkDigitalTwinImage, watermarkGeminiImage, watermarkHologramAudio };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Image Watermarking Service
|
|
5
|
+
*
|
|
6
|
+
* Embeds INVISIBLE metadata into generated images to indicate
|
|
7
|
+
* "Synthetic Origin" - marking AI-generated images for safety/authenticity.
|
|
8
|
+
*
|
|
9
|
+
* These watermarks are completely invisible to users.
|
|
10
|
+
* They are stored in the PNG file's metadata chunks (iTXt/tEXt),
|
|
11
|
+
* not rendered on the image itself. Similar to EXIF data in photos.
|
|
12
|
+
*
|
|
13
|
+
* For PNG format: Uses ancillary iTXt chunk (international text)
|
|
14
|
+
* For JPEG format: Could use EXIF/XMP metadata (future)
|
|
15
|
+
* For WebP format: Could use XMP metadata (future)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Watermark configuration
|
|
20
|
+
*/
|
|
21
|
+
interface ImageWatermarkConfig {
|
|
22
|
+
/** Platform identifier */
|
|
23
|
+
platform: string;
|
|
24
|
+
/** Timestamp of generation */
|
|
25
|
+
timestamp: number;
|
|
26
|
+
/** Source system (e.g., 'dalle', 'imagen', 'gemini', 'digital_twin') */
|
|
27
|
+
source: string;
|
|
28
|
+
/** Optional user ID (hashed) for audit trail */
|
|
29
|
+
userIdHash?: string;
|
|
30
|
+
/** Optional model name */
|
|
31
|
+
model?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Embed INVISIBLE watermark into image buffer
|
|
35
|
+
*
|
|
36
|
+
* For PNG: Inserts iTXt chunk before IEND
|
|
37
|
+
* For other formats: Passes through unchanged (future: EXIF/XMP)
|
|
38
|
+
*
|
|
39
|
+
* The watermark is stored in metadata, NOT visible on the image.
|
|
40
|
+
*
|
|
41
|
+
* @param imageBuffer - Raw image data
|
|
42
|
+
* @param format - Image format (png, jpeg, webp)
|
|
43
|
+
* @param config - Watermark configuration
|
|
44
|
+
* @returns Watermarked image buffer
|
|
45
|
+
*/
|
|
46
|
+
declare function embedImageWatermark(imageBuffer: Uint8Array, format?: string, config?: Partial<ImageWatermarkConfig>): Buffer;
|
|
47
|
+
/**
|
|
48
|
+
* Detect synthetic origin watermark from image
|
|
49
|
+
*
|
|
50
|
+
* @param imageBuffer - Image data to check
|
|
51
|
+
* @returns Watermark info if found, null otherwise
|
|
52
|
+
*/
|
|
53
|
+
declare function detectSyntheticImageWatermark(imageBuffer: Buffer): ImageWatermarkConfig | null;
|
|
54
|
+
/**
|
|
55
|
+
* Convenience function for DALL-E generated images
|
|
56
|
+
*/
|
|
57
|
+
declare function watermarkDallEImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
58
|
+
/**
|
|
59
|
+
* Convenience function for Gemini Imagen images
|
|
60
|
+
*/
|
|
61
|
+
declare function watermarkGeminiImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
62
|
+
/**
|
|
63
|
+
* Convenience function for Digital Twin generated images
|
|
64
|
+
*/
|
|
65
|
+
declare function watermarkDigitalTwinImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
66
|
+
/**
|
|
67
|
+
* Convenience function for Cyrano-generated images
|
|
68
|
+
*/
|
|
69
|
+
declare function watermarkCyranoImage(imageBuffer: Uint8Array, format?: string, userIdHash?: string): Buffer;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Audio Watermarking Service
|
|
73
|
+
*
|
|
74
|
+
* Embeds invisible/inaudible metadata into synthesized audio to indicate
|
|
75
|
+
* "Synthetic Origin" - marking AI-generated audio for safety and authenticity.
|
|
76
|
+
*
|
|
77
|
+
* Approach: Adds a metadata comment to the MP3 ID3 tag or embeds a
|
|
78
|
+
* high-frequency inaudible tone pattern (above 18kHz) that serves as
|
|
79
|
+
* a digital fingerprint.
|
|
80
|
+
*
|
|
81
|
+
* For MP3 format: Uses ID3 tag metadata injection
|
|
82
|
+
* For raw formats: Could use frequency-domain watermarking (future)
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Watermark configuration
|
|
87
|
+
*/
|
|
88
|
+
interface WatermarkConfig {
|
|
89
|
+
/** Platform identifier */
|
|
90
|
+
platform: string;
|
|
91
|
+
/** Timestamp of generation */
|
|
92
|
+
timestamp: number;
|
|
93
|
+
/** Source system (e.g., 'cyrano', 'hologram', 'twin') */
|
|
94
|
+
source: string;
|
|
95
|
+
/** Optional user ID (hashed) for audit trail */
|
|
96
|
+
userIdHash?: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Embed watermark into audio buffer
|
|
100
|
+
*
|
|
101
|
+
* For MP3: Prepends ID3v2 tag with synthetic origin metadata
|
|
102
|
+
* For other formats: Passes through unchanged (future: frequency watermarking)
|
|
103
|
+
*
|
|
104
|
+
* @param audioBuffer - Raw audio data
|
|
105
|
+
* @param format - Audio format (mp3, wav, etc.)
|
|
106
|
+
* @param config - Watermark configuration
|
|
107
|
+
* @returns Watermarked audio buffer
|
|
108
|
+
*/
|
|
109
|
+
declare function embedAudioWatermark(audioBuffer: Buffer, format?: string, config?: Partial<WatermarkConfig>): Buffer;
|
|
110
|
+
/**
|
|
111
|
+
* Check if audio has synthetic origin watermark
|
|
112
|
+
*
|
|
113
|
+
* @param audioBuffer - Audio data to check
|
|
114
|
+
* @returns Watermark info if found, null otherwise
|
|
115
|
+
*/
|
|
116
|
+
declare function detectSyntheticWatermark(audioBuffer: Buffer): WatermarkConfig | null;
|
|
117
|
+
/**
|
|
118
|
+
* Convenience function for Digital Twin audio
|
|
119
|
+
*/
|
|
120
|
+
declare function watermarkDigitalTwinAudio(audioBuffer: Buffer, format?: string, userIdHash?: string): Buffer;
|
|
121
|
+
/**
|
|
122
|
+
* Convenience function for Hologram audio
|
|
123
|
+
*/
|
|
124
|
+
declare function watermarkHologramAudio(audioBuffer: Buffer, format?: string): Buffer;
|
|
125
|
+
/**
|
|
126
|
+
* Convenience function for Cyrano TTS audio
|
|
127
|
+
*/
|
|
128
|
+
declare function watermarkCyranoAudio(audioBuffer: Buffer, format?: string): Buffer;
|
|
129
|
+
|
|
130
|
+
export { type ImageWatermarkConfig, type WatermarkConfig, detectSyntheticImageWatermark, detectSyntheticWatermark, embedAudioWatermark, embedImageWatermark, watermarkCyranoAudio, watermarkCyranoImage, watermarkDallEImage, watermarkDigitalTwinAudio, watermarkDigitalTwinImage, watermarkGeminiImage, watermarkHologramAudio };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
detectSyntheticImageWatermark: () => detectSyntheticImageWatermark,
|
|
24
|
+
detectSyntheticWatermark: () => detectSyntheticWatermark,
|
|
25
|
+
embedAudioWatermark: () => embedAudioWatermark,
|
|
26
|
+
embedImageWatermark: () => embedImageWatermark,
|
|
27
|
+
watermarkCyranoAudio: () => watermarkCyranoAudio,
|
|
28
|
+
watermarkCyranoImage: () => watermarkCyranoImage,
|
|
29
|
+
watermarkDallEImage: () => watermarkDallEImage,
|
|
30
|
+
watermarkDigitalTwinAudio: () => watermarkDigitalTwinAudio,
|
|
31
|
+
watermarkDigitalTwinImage: () => watermarkDigitalTwinImage,
|
|
32
|
+
watermarkGeminiImage: () => watermarkGeminiImage,
|
|
33
|
+
watermarkHologramAudio: () => watermarkHologramAudio
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/imageWatermark.ts
|
|
38
|
+
var import_buffer = require("buffer");
|
|
39
|
+
var DEFAULT_WATERMARK = {
|
|
40
|
+
platform: "SYNTHETIC",
|
|
41
|
+
source: "ai_generated"
|
|
42
|
+
};
|
|
43
|
+
var PNG_SIGNATURE = import_buffer.Buffer.from([
|
|
44
|
+
137,
|
|
45
|
+
80,
|
|
46
|
+
78,
|
|
47
|
+
71,
|
|
48
|
+
13,
|
|
49
|
+
10,
|
|
50
|
+
26,
|
|
51
|
+
10
|
|
52
|
+
]);
|
|
53
|
+
function isPNG(buffer) {
|
|
54
|
+
if (buffer.length < 8) return false;
|
|
55
|
+
for (let i = 0; i < 8; i++) {
|
|
56
|
+
if (buffer[i] !== PNG_SIGNATURE[i]) return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function crc32(data) {
|
|
61
|
+
const table = [];
|
|
62
|
+
for (let n = 0; n < 256; n++) {
|
|
63
|
+
let c = n;
|
|
64
|
+
for (let k = 0; k < 8; k++) {
|
|
65
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
66
|
+
}
|
|
67
|
+
table[n] = c >>> 0;
|
|
68
|
+
}
|
|
69
|
+
let crc = 4294967295;
|
|
70
|
+
for (let i = 0; i < data.length; i++) {
|
|
71
|
+
crc = table[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
72
|
+
}
|
|
73
|
+
return (crc ^ 4294967295) >>> 0;
|
|
74
|
+
}
|
|
75
|
+
function createiTXtChunk(config) {
|
|
76
|
+
const keyword = "SyntheticOrigin";
|
|
77
|
+
const text = `SYNTHETIC_IMAGE|${config.platform}|${config.source}|${config.timestamp}|${config.userIdHash || ""}|${config.model || ""}`;
|
|
78
|
+
const keywordBuf = import_buffer.Buffer.from(keyword + "\0");
|
|
79
|
+
const compressionFlag = import_buffer.Buffer.from([0]);
|
|
80
|
+
const compressionMethod = import_buffer.Buffer.from([0]);
|
|
81
|
+
const languageTag = import_buffer.Buffer.from("en\0");
|
|
82
|
+
const translatedKeyword = import_buffer.Buffer.from("\0");
|
|
83
|
+
const textBuf = import_buffer.Buffer.from(text);
|
|
84
|
+
const chunkData = import_buffer.Buffer.concat([
|
|
85
|
+
keywordBuf,
|
|
86
|
+
compressionFlag,
|
|
87
|
+
compressionMethod,
|
|
88
|
+
languageTag,
|
|
89
|
+
translatedKeyword,
|
|
90
|
+
textBuf
|
|
91
|
+
]);
|
|
92
|
+
const chunkType = import_buffer.Buffer.from("iTXt");
|
|
93
|
+
const typeAndData = import_buffer.Buffer.concat([
|
|
94
|
+
chunkType,
|
|
95
|
+
chunkData
|
|
96
|
+
]);
|
|
97
|
+
const crcValue = crc32(new Uint8Array(typeAndData));
|
|
98
|
+
const crcBuf = import_buffer.Buffer.alloc(4);
|
|
99
|
+
crcBuf.writeUInt32BE(crcValue, 0);
|
|
100
|
+
const lengthBuf = import_buffer.Buffer.alloc(4);
|
|
101
|
+
lengthBuf.writeUInt32BE(chunkData.length, 0);
|
|
102
|
+
const parts = [
|
|
103
|
+
lengthBuf,
|
|
104
|
+
chunkType,
|
|
105
|
+
chunkData,
|
|
106
|
+
crcBuf
|
|
107
|
+
];
|
|
108
|
+
return import_buffer.Buffer.concat(parts);
|
|
109
|
+
}
|
|
110
|
+
function findIENDPosition(buffer) {
|
|
111
|
+
const iendMarker = import_buffer.Buffer.from("IEND");
|
|
112
|
+
for (let i = 8; i < buffer.length - 8; i++) {
|
|
113
|
+
const slice = buffer.subarray(i + 4, i + 8);
|
|
114
|
+
if (import_buffer.Buffer.from(slice).equals(iendMarker)) {
|
|
115
|
+
return i;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
function embedImageWatermark(imageBuffer, format = "png", config = {}) {
|
|
121
|
+
const fullConfig = {
|
|
122
|
+
...DEFAULT_WATERMARK,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
...config
|
|
125
|
+
};
|
|
126
|
+
const normalizedFormat = format.toLowerCase().replace("image/", "");
|
|
127
|
+
if (normalizedFormat !== "png") {
|
|
128
|
+
return import_buffer.Buffer.from(imageBuffer);
|
|
129
|
+
}
|
|
130
|
+
if (!isPNG(imageBuffer)) {
|
|
131
|
+
return import_buffer.Buffer.from(imageBuffer);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const iendPos = findIENDPosition(imageBuffer);
|
|
135
|
+
if (iendPos === -1) {
|
|
136
|
+
return import_buffer.Buffer.from(imageBuffer);
|
|
137
|
+
}
|
|
138
|
+
const watermarkChunk = createiTXtChunk(fullConfig);
|
|
139
|
+
const beforeIEND = imageBuffer.subarray(0, iendPos);
|
|
140
|
+
const iendAndAfter = imageBuffer.subarray(iendPos);
|
|
141
|
+
const parts = [
|
|
142
|
+
import_buffer.Buffer.from(beforeIEND),
|
|
143
|
+
watermarkChunk,
|
|
144
|
+
import_buffer.Buffer.from(iendAndAfter)
|
|
145
|
+
];
|
|
146
|
+
return import_buffer.Buffer.concat(parts);
|
|
147
|
+
} catch {
|
|
148
|
+
return import_buffer.Buffer.from(imageBuffer);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function detectSyntheticImageWatermark(imageBuffer) {
|
|
152
|
+
const bufferArray = new Uint8Array(imageBuffer);
|
|
153
|
+
if (!isPNG(bufferArray) || imageBuffer.length < 20) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const itxtMarker = import_buffer.Buffer.from("iTXt");
|
|
158
|
+
let pos = 8;
|
|
159
|
+
while (pos < imageBuffer.length - 12) {
|
|
160
|
+
const chunkLength = imageBuffer.readUInt32BE(pos);
|
|
161
|
+
const chunkType = imageBuffer.subarray(pos + 4, pos + 8).toString();
|
|
162
|
+
if (chunkType === "iTXt") {
|
|
163
|
+
const chunkData = imageBuffer.subarray(pos + 8, pos + 8 + chunkLength);
|
|
164
|
+
const chunkStr = chunkData.toString();
|
|
165
|
+
if (chunkStr.includes("SyntheticOrigin") && chunkStr.includes("SYNTHETIC_IMAGE|")) {
|
|
166
|
+
const markerIdx = chunkStr.indexOf("SYNTHETIC_IMAGE|");
|
|
167
|
+
const text = chunkStr.substring(markerIdx);
|
|
168
|
+
const parts = text.split("|");
|
|
169
|
+
if (parts.length >= 4) {
|
|
170
|
+
return {
|
|
171
|
+
platform: parts[1],
|
|
172
|
+
source: parts[2],
|
|
173
|
+
timestamp: parseInt(parts[3], 10),
|
|
174
|
+
userIdHash: parts[4] || void 0,
|
|
175
|
+
model: parts[5] || void 0
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
pos += 4 + 4 + chunkLength + 4;
|
|
181
|
+
if (chunkType === "IEND") break;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function watermarkDallEImage(imageBuffer, format = "png", userIdHash) {
|
|
188
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
189
|
+
source: "dalle",
|
|
190
|
+
model: "dall-e",
|
|
191
|
+
userIdHash
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function watermarkGeminiImage(imageBuffer, format = "png", userIdHash) {
|
|
195
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
196
|
+
source: "gemini",
|
|
197
|
+
model: "imagen",
|
|
198
|
+
userIdHash
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function watermarkDigitalTwinImage(imageBuffer, format = "png", userIdHash) {
|
|
202
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
203
|
+
source: "digital_twin",
|
|
204
|
+
userIdHash
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function watermarkCyranoImage(imageBuffer, format = "png", userIdHash) {
|
|
208
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
209
|
+
source: "cyrano",
|
|
210
|
+
userIdHash
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/audioWatermark.ts
|
|
215
|
+
var import_buffer2 = require("buffer");
|
|
216
|
+
var DEFAULT_WATERMARK2 = {
|
|
217
|
+
platform: "SYNTHETIC",
|
|
218
|
+
source: "tts"
|
|
219
|
+
};
|
|
220
|
+
function createID3WatermarkTag(config) {
|
|
221
|
+
const comment = `SYNTHETIC_AUDIO|${config.platform}|${config.source}|${config.timestamp}${config.userIdHash ? `|${config.userIdHash}` : ""}`;
|
|
222
|
+
const encoding = import_buffer2.Buffer.from([0]);
|
|
223
|
+
const language = import_buffer2.Buffer.from("eng");
|
|
224
|
+
const shortDesc = import_buffer2.Buffer.from("SYNTHETIC_ORIGIN\0", "binary");
|
|
225
|
+
const text = import_buffer2.Buffer.concat([
|
|
226
|
+
import_buffer2.Buffer.from(comment, "utf-8"),
|
|
227
|
+
import_buffer2.Buffer.from([0])
|
|
228
|
+
]);
|
|
229
|
+
const frameContent = import_buffer2.Buffer.concat([
|
|
230
|
+
encoding,
|
|
231
|
+
language,
|
|
232
|
+
shortDesc,
|
|
233
|
+
text
|
|
234
|
+
]);
|
|
235
|
+
const frameId = import_buffer2.Buffer.from("COMM");
|
|
236
|
+
const frameSize = import_buffer2.Buffer.alloc(4);
|
|
237
|
+
frameSize.writeUInt32BE(frameContent.length, 0);
|
|
238
|
+
const frameFlags = import_buffer2.Buffer.from([0, 0]);
|
|
239
|
+
const frame = import_buffer2.Buffer.concat([
|
|
240
|
+
frameId,
|
|
241
|
+
frameSize,
|
|
242
|
+
frameFlags,
|
|
243
|
+
frameContent
|
|
244
|
+
]);
|
|
245
|
+
const id3Header = import_buffer2.Buffer.from([
|
|
246
|
+
73,
|
|
247
|
+
68,
|
|
248
|
+
51,
|
|
249
|
+
// "ID3"
|
|
250
|
+
3,
|
|
251
|
+
0,
|
|
252
|
+
// Version 2.3
|
|
253
|
+
0
|
|
254
|
+
// Flags
|
|
255
|
+
]);
|
|
256
|
+
const size = frame.length;
|
|
257
|
+
const syncsafeSize = import_buffer2.Buffer.alloc(4);
|
|
258
|
+
syncsafeSize[0] = size >> 21 & 127;
|
|
259
|
+
syncsafeSize[1] = size >> 14 & 127;
|
|
260
|
+
syncsafeSize[2] = size >> 7 & 127;
|
|
261
|
+
syncsafeSize[3] = size & 127;
|
|
262
|
+
return import_buffer2.Buffer.concat([
|
|
263
|
+
id3Header,
|
|
264
|
+
syncsafeSize,
|
|
265
|
+
frame
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
function hasID3Tag(audioBuffer) {
|
|
269
|
+
if (audioBuffer.length < 10) return false;
|
|
270
|
+
return audioBuffer[0] === 73 && // 'I'
|
|
271
|
+
audioBuffer[1] === 68 && // 'D'
|
|
272
|
+
audioBuffer[2] === 51;
|
|
273
|
+
}
|
|
274
|
+
function getID3TagSize(audioBuffer) {
|
|
275
|
+
if (!hasID3Tag(audioBuffer)) return 0;
|
|
276
|
+
const size = (audioBuffer[6] & 127) << 21 | (audioBuffer[7] & 127) << 14 | (audioBuffer[8] & 127) << 7 | audioBuffer[9] & 127;
|
|
277
|
+
return size + 10;
|
|
278
|
+
}
|
|
279
|
+
function embedAudioWatermark(audioBuffer, format = "mp3", config = {}) {
|
|
280
|
+
const fullConfig = {
|
|
281
|
+
...DEFAULT_WATERMARK2,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
...config
|
|
284
|
+
};
|
|
285
|
+
const normalizedFormat = format.toLowerCase();
|
|
286
|
+
if (normalizedFormat !== "mp3") {
|
|
287
|
+
return audioBuffer;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
let audioData = audioBuffer;
|
|
291
|
+
const bufferArray = new Uint8Array(audioBuffer);
|
|
292
|
+
if (hasID3Tag(bufferArray)) {
|
|
293
|
+
const existingTagSize = getID3TagSize(bufferArray);
|
|
294
|
+
if (existingTagSize <= audioBuffer.length) {
|
|
295
|
+
audioData = audioBuffer.subarray(existingTagSize);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const watermarkTag = createID3WatermarkTag(fullConfig);
|
|
299
|
+
return import_buffer2.Buffer.concat([watermarkTag, audioData]);
|
|
300
|
+
} catch {
|
|
301
|
+
return audioBuffer;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function detectSyntheticWatermark(audioBuffer) {
|
|
305
|
+
const bufferArray = new Uint8Array(audioBuffer);
|
|
306
|
+
if (!hasID3Tag(bufferArray)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const searchStr = "SYNTHETIC_ORIGIN";
|
|
311
|
+
const idx = audioBuffer.indexOf(searchStr);
|
|
312
|
+
if (idx === -1) return null;
|
|
313
|
+
const markerEnd = idx + searchStr.length + 1;
|
|
314
|
+
if (markerEnd >= audioBuffer.length) return null;
|
|
315
|
+
const markerStart = audioBuffer.indexOf("SYNTHETIC_AUDIO|", markerEnd);
|
|
316
|
+
if (markerStart === -1) return null;
|
|
317
|
+
let end = markerStart;
|
|
318
|
+
while (end < audioBuffer.length && audioBuffer[end] !== 0) {
|
|
319
|
+
end++;
|
|
320
|
+
}
|
|
321
|
+
const comment = audioBuffer.subarray(markerStart, end).toString("utf-8");
|
|
322
|
+
const parts = comment.split("|");
|
|
323
|
+
if (parts.length >= 4) {
|
|
324
|
+
return {
|
|
325
|
+
platform: parts[1] || "",
|
|
326
|
+
source: parts[2] || "",
|
|
327
|
+
timestamp: parseInt(parts[3] || "0", 10),
|
|
328
|
+
userIdHash: parts[4]
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function watermarkDigitalTwinAudio(audioBuffer, format = "mp3", userIdHash) {
|
|
336
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
337
|
+
source: "digital_twin",
|
|
338
|
+
userIdHash
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function watermarkHologramAudio(audioBuffer, format = "mp3") {
|
|
342
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
343
|
+
source: "hologram"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function watermarkCyranoAudio(audioBuffer, format = "mp3") {
|
|
347
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
348
|
+
source: "cyrano"
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
352
|
+
0 && (module.exports = {
|
|
353
|
+
detectSyntheticImageWatermark,
|
|
354
|
+
detectSyntheticWatermark,
|
|
355
|
+
embedAudioWatermark,
|
|
356
|
+
embedImageWatermark,
|
|
357
|
+
watermarkCyranoAudio,
|
|
358
|
+
watermarkCyranoImage,
|
|
359
|
+
watermarkDallEImage,
|
|
360
|
+
watermarkDigitalTwinAudio,
|
|
361
|
+
watermarkDigitalTwinImage,
|
|
362
|
+
watermarkGeminiImage,
|
|
363
|
+
watermarkHologramAudio
|
|
364
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// src/imageWatermark.ts
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
var DEFAULT_WATERMARK = {
|
|
4
|
+
platform: "SYNTHETIC",
|
|
5
|
+
source: "ai_generated"
|
|
6
|
+
};
|
|
7
|
+
var PNG_SIGNATURE = Buffer.from([
|
|
8
|
+
137,
|
|
9
|
+
80,
|
|
10
|
+
78,
|
|
11
|
+
71,
|
|
12
|
+
13,
|
|
13
|
+
10,
|
|
14
|
+
26,
|
|
15
|
+
10
|
|
16
|
+
]);
|
|
17
|
+
function isPNG(buffer) {
|
|
18
|
+
if (buffer.length < 8) return false;
|
|
19
|
+
for (let i = 0; i < 8; i++) {
|
|
20
|
+
if (buffer[i] !== PNG_SIGNATURE[i]) return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function crc32(data) {
|
|
25
|
+
const table = [];
|
|
26
|
+
for (let n = 0; n < 256; n++) {
|
|
27
|
+
let c = n;
|
|
28
|
+
for (let k = 0; k < 8; k++) {
|
|
29
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
30
|
+
}
|
|
31
|
+
table[n] = c >>> 0;
|
|
32
|
+
}
|
|
33
|
+
let crc = 4294967295;
|
|
34
|
+
for (let i = 0; i < data.length; i++) {
|
|
35
|
+
crc = table[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
36
|
+
}
|
|
37
|
+
return (crc ^ 4294967295) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
function createiTXtChunk(config) {
|
|
40
|
+
const keyword = "SyntheticOrigin";
|
|
41
|
+
const text = `SYNTHETIC_IMAGE|${config.platform}|${config.source}|${config.timestamp}|${config.userIdHash || ""}|${config.model || ""}`;
|
|
42
|
+
const keywordBuf = Buffer.from(keyword + "\0");
|
|
43
|
+
const compressionFlag = Buffer.from([0]);
|
|
44
|
+
const compressionMethod = Buffer.from([0]);
|
|
45
|
+
const languageTag = Buffer.from("en\0");
|
|
46
|
+
const translatedKeyword = Buffer.from("\0");
|
|
47
|
+
const textBuf = Buffer.from(text);
|
|
48
|
+
const chunkData = Buffer.concat([
|
|
49
|
+
keywordBuf,
|
|
50
|
+
compressionFlag,
|
|
51
|
+
compressionMethod,
|
|
52
|
+
languageTag,
|
|
53
|
+
translatedKeyword,
|
|
54
|
+
textBuf
|
|
55
|
+
]);
|
|
56
|
+
const chunkType = Buffer.from("iTXt");
|
|
57
|
+
const typeAndData = Buffer.concat([
|
|
58
|
+
chunkType,
|
|
59
|
+
chunkData
|
|
60
|
+
]);
|
|
61
|
+
const crcValue = crc32(new Uint8Array(typeAndData));
|
|
62
|
+
const crcBuf = Buffer.alloc(4);
|
|
63
|
+
crcBuf.writeUInt32BE(crcValue, 0);
|
|
64
|
+
const lengthBuf = Buffer.alloc(4);
|
|
65
|
+
lengthBuf.writeUInt32BE(chunkData.length, 0);
|
|
66
|
+
const parts = [
|
|
67
|
+
lengthBuf,
|
|
68
|
+
chunkType,
|
|
69
|
+
chunkData,
|
|
70
|
+
crcBuf
|
|
71
|
+
];
|
|
72
|
+
return Buffer.concat(parts);
|
|
73
|
+
}
|
|
74
|
+
function findIENDPosition(buffer) {
|
|
75
|
+
const iendMarker = Buffer.from("IEND");
|
|
76
|
+
for (let i = 8; i < buffer.length - 8; i++) {
|
|
77
|
+
const slice = buffer.subarray(i + 4, i + 8);
|
|
78
|
+
if (Buffer.from(slice).equals(iendMarker)) {
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
function embedImageWatermark(imageBuffer, format = "png", config = {}) {
|
|
85
|
+
const fullConfig = {
|
|
86
|
+
...DEFAULT_WATERMARK,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
...config
|
|
89
|
+
};
|
|
90
|
+
const normalizedFormat = format.toLowerCase().replace("image/", "");
|
|
91
|
+
if (normalizedFormat !== "png") {
|
|
92
|
+
return Buffer.from(imageBuffer);
|
|
93
|
+
}
|
|
94
|
+
if (!isPNG(imageBuffer)) {
|
|
95
|
+
return Buffer.from(imageBuffer);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const iendPos = findIENDPosition(imageBuffer);
|
|
99
|
+
if (iendPos === -1) {
|
|
100
|
+
return Buffer.from(imageBuffer);
|
|
101
|
+
}
|
|
102
|
+
const watermarkChunk = createiTXtChunk(fullConfig);
|
|
103
|
+
const beforeIEND = imageBuffer.subarray(0, iendPos);
|
|
104
|
+
const iendAndAfter = imageBuffer.subarray(iendPos);
|
|
105
|
+
const parts = [
|
|
106
|
+
Buffer.from(beforeIEND),
|
|
107
|
+
watermarkChunk,
|
|
108
|
+
Buffer.from(iendAndAfter)
|
|
109
|
+
];
|
|
110
|
+
return Buffer.concat(parts);
|
|
111
|
+
} catch {
|
|
112
|
+
return Buffer.from(imageBuffer);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function detectSyntheticImageWatermark(imageBuffer) {
|
|
116
|
+
const bufferArray = new Uint8Array(imageBuffer);
|
|
117
|
+
if (!isPNG(bufferArray) || imageBuffer.length < 20) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const itxtMarker = Buffer.from("iTXt");
|
|
122
|
+
let pos = 8;
|
|
123
|
+
while (pos < imageBuffer.length - 12) {
|
|
124
|
+
const chunkLength = imageBuffer.readUInt32BE(pos);
|
|
125
|
+
const chunkType = imageBuffer.subarray(pos + 4, pos + 8).toString();
|
|
126
|
+
if (chunkType === "iTXt") {
|
|
127
|
+
const chunkData = imageBuffer.subarray(pos + 8, pos + 8 + chunkLength);
|
|
128
|
+
const chunkStr = chunkData.toString();
|
|
129
|
+
if (chunkStr.includes("SyntheticOrigin") && chunkStr.includes("SYNTHETIC_IMAGE|")) {
|
|
130
|
+
const markerIdx = chunkStr.indexOf("SYNTHETIC_IMAGE|");
|
|
131
|
+
const text = chunkStr.substring(markerIdx);
|
|
132
|
+
const parts = text.split("|");
|
|
133
|
+
if (parts.length >= 4) {
|
|
134
|
+
return {
|
|
135
|
+
platform: parts[1],
|
|
136
|
+
source: parts[2],
|
|
137
|
+
timestamp: parseInt(parts[3], 10),
|
|
138
|
+
userIdHash: parts[4] || void 0,
|
|
139
|
+
model: parts[5] || void 0
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
pos += 4 + 4 + chunkLength + 4;
|
|
145
|
+
if (chunkType === "IEND") break;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function watermarkDallEImage(imageBuffer, format = "png", userIdHash) {
|
|
152
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
153
|
+
source: "dalle",
|
|
154
|
+
model: "dall-e",
|
|
155
|
+
userIdHash
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function watermarkGeminiImage(imageBuffer, format = "png", userIdHash) {
|
|
159
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
160
|
+
source: "gemini",
|
|
161
|
+
model: "imagen",
|
|
162
|
+
userIdHash
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function watermarkDigitalTwinImage(imageBuffer, format = "png", userIdHash) {
|
|
166
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
167
|
+
source: "digital_twin",
|
|
168
|
+
userIdHash
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function watermarkCyranoImage(imageBuffer, format = "png", userIdHash) {
|
|
172
|
+
return embedImageWatermark(imageBuffer, format, {
|
|
173
|
+
source: "cyrano",
|
|
174
|
+
userIdHash
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/audioWatermark.ts
|
|
179
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
180
|
+
var DEFAULT_WATERMARK2 = {
|
|
181
|
+
platform: "SYNTHETIC",
|
|
182
|
+
source: "tts"
|
|
183
|
+
};
|
|
184
|
+
function createID3WatermarkTag(config) {
|
|
185
|
+
const comment = `SYNTHETIC_AUDIO|${config.platform}|${config.source}|${config.timestamp}${config.userIdHash ? `|${config.userIdHash}` : ""}`;
|
|
186
|
+
const encoding = Buffer2.from([0]);
|
|
187
|
+
const language = Buffer2.from("eng");
|
|
188
|
+
const shortDesc = Buffer2.from("SYNTHETIC_ORIGIN\0", "binary");
|
|
189
|
+
const text = Buffer2.concat([
|
|
190
|
+
Buffer2.from(comment, "utf-8"),
|
|
191
|
+
Buffer2.from([0])
|
|
192
|
+
]);
|
|
193
|
+
const frameContent = Buffer2.concat([
|
|
194
|
+
encoding,
|
|
195
|
+
language,
|
|
196
|
+
shortDesc,
|
|
197
|
+
text
|
|
198
|
+
]);
|
|
199
|
+
const frameId = Buffer2.from("COMM");
|
|
200
|
+
const frameSize = Buffer2.alloc(4);
|
|
201
|
+
frameSize.writeUInt32BE(frameContent.length, 0);
|
|
202
|
+
const frameFlags = Buffer2.from([0, 0]);
|
|
203
|
+
const frame = Buffer2.concat([
|
|
204
|
+
frameId,
|
|
205
|
+
frameSize,
|
|
206
|
+
frameFlags,
|
|
207
|
+
frameContent
|
|
208
|
+
]);
|
|
209
|
+
const id3Header = Buffer2.from([
|
|
210
|
+
73,
|
|
211
|
+
68,
|
|
212
|
+
51,
|
|
213
|
+
// "ID3"
|
|
214
|
+
3,
|
|
215
|
+
0,
|
|
216
|
+
// Version 2.3
|
|
217
|
+
0
|
|
218
|
+
// Flags
|
|
219
|
+
]);
|
|
220
|
+
const size = frame.length;
|
|
221
|
+
const syncsafeSize = Buffer2.alloc(4);
|
|
222
|
+
syncsafeSize[0] = size >> 21 & 127;
|
|
223
|
+
syncsafeSize[1] = size >> 14 & 127;
|
|
224
|
+
syncsafeSize[2] = size >> 7 & 127;
|
|
225
|
+
syncsafeSize[3] = size & 127;
|
|
226
|
+
return Buffer2.concat([
|
|
227
|
+
id3Header,
|
|
228
|
+
syncsafeSize,
|
|
229
|
+
frame
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
function hasID3Tag(audioBuffer) {
|
|
233
|
+
if (audioBuffer.length < 10) return false;
|
|
234
|
+
return audioBuffer[0] === 73 && // 'I'
|
|
235
|
+
audioBuffer[1] === 68 && // 'D'
|
|
236
|
+
audioBuffer[2] === 51;
|
|
237
|
+
}
|
|
238
|
+
function getID3TagSize(audioBuffer) {
|
|
239
|
+
if (!hasID3Tag(audioBuffer)) return 0;
|
|
240
|
+
const size = (audioBuffer[6] & 127) << 21 | (audioBuffer[7] & 127) << 14 | (audioBuffer[8] & 127) << 7 | audioBuffer[9] & 127;
|
|
241
|
+
return size + 10;
|
|
242
|
+
}
|
|
243
|
+
function embedAudioWatermark(audioBuffer, format = "mp3", config = {}) {
|
|
244
|
+
const fullConfig = {
|
|
245
|
+
...DEFAULT_WATERMARK2,
|
|
246
|
+
timestamp: Date.now(),
|
|
247
|
+
...config
|
|
248
|
+
};
|
|
249
|
+
const normalizedFormat = format.toLowerCase();
|
|
250
|
+
if (normalizedFormat !== "mp3") {
|
|
251
|
+
return audioBuffer;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
let audioData = audioBuffer;
|
|
255
|
+
const bufferArray = new Uint8Array(audioBuffer);
|
|
256
|
+
if (hasID3Tag(bufferArray)) {
|
|
257
|
+
const existingTagSize = getID3TagSize(bufferArray);
|
|
258
|
+
if (existingTagSize <= audioBuffer.length) {
|
|
259
|
+
audioData = audioBuffer.subarray(existingTagSize);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const watermarkTag = createID3WatermarkTag(fullConfig);
|
|
263
|
+
return Buffer2.concat([watermarkTag, audioData]);
|
|
264
|
+
} catch {
|
|
265
|
+
return audioBuffer;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function detectSyntheticWatermark(audioBuffer) {
|
|
269
|
+
const bufferArray = new Uint8Array(audioBuffer);
|
|
270
|
+
if (!hasID3Tag(bufferArray)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const searchStr = "SYNTHETIC_ORIGIN";
|
|
275
|
+
const idx = audioBuffer.indexOf(searchStr);
|
|
276
|
+
if (idx === -1) return null;
|
|
277
|
+
const markerEnd = idx + searchStr.length + 1;
|
|
278
|
+
if (markerEnd >= audioBuffer.length) return null;
|
|
279
|
+
const markerStart = audioBuffer.indexOf("SYNTHETIC_AUDIO|", markerEnd);
|
|
280
|
+
if (markerStart === -1) return null;
|
|
281
|
+
let end = markerStart;
|
|
282
|
+
while (end < audioBuffer.length && audioBuffer[end] !== 0) {
|
|
283
|
+
end++;
|
|
284
|
+
}
|
|
285
|
+
const comment = audioBuffer.subarray(markerStart, end).toString("utf-8");
|
|
286
|
+
const parts = comment.split("|");
|
|
287
|
+
if (parts.length >= 4) {
|
|
288
|
+
return {
|
|
289
|
+
platform: parts[1] || "",
|
|
290
|
+
source: parts[2] || "",
|
|
291
|
+
timestamp: parseInt(parts[3] || "0", 10),
|
|
292
|
+
userIdHash: parts[4]
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
function watermarkDigitalTwinAudio(audioBuffer, format = "mp3", userIdHash) {
|
|
300
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
301
|
+
source: "digital_twin",
|
|
302
|
+
userIdHash
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function watermarkHologramAudio(audioBuffer, format = "mp3") {
|
|
306
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
307
|
+
source: "hologram"
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function watermarkCyranoAudio(audioBuffer, format = "mp3") {
|
|
311
|
+
return embedAudioWatermark(audioBuffer, format, {
|
|
312
|
+
source: "cyrano"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
export {
|
|
316
|
+
detectSyntheticImageWatermark,
|
|
317
|
+
detectSyntheticWatermark,
|
|
318
|
+
embedAudioWatermark,
|
|
319
|
+
embedImageWatermark,
|
|
320
|
+
watermarkCyranoAudio,
|
|
321
|
+
watermarkCyranoImage,
|
|
322
|
+
watermarkDallEImage,
|
|
323
|
+
watermarkDigitalTwinAudio,
|
|
324
|
+
watermarkDigitalTwinImage,
|
|
325
|
+
watermarkGeminiImage,
|
|
326
|
+
watermarkHologramAudio
|
|
327
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@affectively/synthetic-watermark",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Invisible watermarking for AI-generated content. Embeds synthetic origin markers into PNG images (iTXt chunks) and MP3 audio (ID3v2 tags) for authenticity verification.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"test": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"watermark",
|
|
28
|
+
"ai-generated",
|
|
29
|
+
"synthetic",
|
|
30
|
+
"authenticity",
|
|
31
|
+
"png",
|
|
32
|
+
"mp3",
|
|
33
|
+
"id3",
|
|
34
|
+
"metadata",
|
|
35
|
+
"ai-safety",
|
|
36
|
+
"content-authenticity",
|
|
37
|
+
"digital-watermark",
|
|
38
|
+
"steganography"
|
|
39
|
+
],
|
|
40
|
+
"author": "AFFECTIVELY <opensource@affectively.ai>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/affectively-ai/synthetic-watermark.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/affectively-ai/synthetic-watermark/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/affectively-ai/synthetic-watermark#readme",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.9.0"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|