@mixpeek/prebid 1.0.0 → 1.0.2
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 +1 -1
- package/ENDPOINTS.md +21 -27
- package/QUICKSTART.md +5 -5
- package/README.md +225 -333
- package/dist/mixpeekContextAdapter.js +1 -1
- package/dist/mixpeekContextAdapter.js.map +1 -1
- package/docs/MIGRATION_V2.md +4 -4
- package/docs/api-reference.md +1 -1
- package/docs/health-check.md +5 -5
- package/docs/integration-guide.md +5 -5
- package/package.json +10 -4
- package/src/api/mixpeekClient.js +133 -32
- package/src/config/constants.js +13 -8
- package/src/modules/mixpeekContextAdapter.js +55 -3
package/docs/MIGRATION_V2.md
CHANGED
|
@@ -40,7 +40,7 @@ pbjs.setConfig({
|
|
|
40
40
|
mixpeek: {
|
|
41
41
|
apiKey: 'sk_your_api_key',
|
|
42
42
|
collectionId: 'col_your_collection',
|
|
43
|
-
endpoint: 'https://
|
|
43
|
+
endpoint: 'https://api.mixpeek.com',
|
|
44
44
|
namespace: 'production',
|
|
45
45
|
featureExtractors: ['taxonomy', 'brand-safety'],
|
|
46
46
|
mode: 'auto',
|
|
@@ -63,7 +63,7 @@ pbjs.setConfig({
|
|
|
63
63
|
params: { // NEW: Wrap all config in params
|
|
64
64
|
apiKey: 'sk_your_api_key',
|
|
65
65
|
collectionId: 'col_your_collection',
|
|
66
|
-
endpoint: 'https://
|
|
66
|
+
endpoint: 'https://api.mixpeek.com',
|
|
67
67
|
namespace: 'production',
|
|
68
68
|
featureExtractors: ['taxonomy', 'brand-safety'],
|
|
69
69
|
mode: 'auto',
|
|
@@ -427,7 +427,7 @@ pbjs.setConfig({
|
|
|
427
427
|
### 2. Revert Package
|
|
428
428
|
|
|
429
429
|
```bash
|
|
430
|
-
npm install @mixpeek/prebid
|
|
430
|
+
npm install @mixpeek/prebid@1.x
|
|
431
431
|
```
|
|
432
432
|
|
|
433
433
|
### 3. Clear Cache
|
|
@@ -487,7 +487,7 @@ Need help with migration?
|
|
|
487
487
|
|
|
488
488
|
- **Documentation:** [Full Integration Guide](integration-guide.md)
|
|
489
489
|
- **Examples:** See `examples/` directory
|
|
490
|
-
- **Issues:** [GitHub Issues](https://github.com/mixpeek/prebid
|
|
490
|
+
- **Issues:** [GitHub Issues](https://github.com/mixpeek/prebid/issues)
|
|
491
491
|
- **Email:** support@mixpeek.com
|
|
492
492
|
- **Slack:** [Join our Slack](https://mixpeek.com/slack)
|
|
493
493
|
|
package/docs/api-reference.md
CHANGED
|
@@ -451,5 +451,5 @@ For questions or issues:
|
|
|
451
451
|
|
|
452
452
|
- **Documentation**: [docs.mixpeek.com](https://docs.mixpeek.com)
|
|
453
453
|
- **Email**: support@mixpeek.com
|
|
454
|
-
- **GitHub**: [github.com/mixpeek/prebid
|
|
454
|
+
- **GitHub**: [github.com/mixpeek/prebid](https://github.com/mixpeek/prebid)
|
|
455
455
|
|
package/docs/health-check.md
CHANGED
|
@@ -164,7 +164,7 @@ if (health.status === 'ok') {
|
|
|
164
164
|
```javascript
|
|
165
165
|
pbjs.setConfig({
|
|
166
166
|
mixpeek: {
|
|
167
|
-
endpoint: 'https://
|
|
167
|
+
endpoint: 'https://api.mixpeek.com',
|
|
168
168
|
healthCheck: 'eager', // Validate immediately
|
|
169
169
|
debug: true,
|
|
170
170
|
timeout: 5000
|
|
@@ -177,7 +177,7 @@ pbjs.setConfig({
|
|
|
177
177
|
```javascript
|
|
178
178
|
pbjs.setConfig({
|
|
179
179
|
mixpeek: {
|
|
180
|
-
endpoint: 'https://
|
|
180
|
+
endpoint: 'https://api.mixpeek.com',
|
|
181
181
|
healthCheck: 'lazy', // Balance validation & performance
|
|
182
182
|
debug: true,
|
|
183
183
|
timeout: 3000
|
|
@@ -251,11 +251,11 @@ This ensures **resilient behavior** where API issues never break your ads.
|
|
|
251
251
|
|
|
252
252
|
```bash
|
|
253
253
|
# Test endpoint manually
|
|
254
|
-
curl https://
|
|
254
|
+
curl https://api.mixpeek.com/v1/health
|
|
255
255
|
|
|
256
256
|
# With authentication
|
|
257
257
|
curl -H "Authorization: Bearer YOUR_API_KEY" \
|
|
258
|
-
https://
|
|
258
|
+
https://api.mixpeek.com/v1/health
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
### Enable Debug Logging
|
|
@@ -272,7 +272,7 @@ pbjs.setConfig({
|
|
|
272
272
|
Console output:
|
|
273
273
|
```
|
|
274
274
|
[mixpeek] Performing health check...
|
|
275
|
-
[mixpeek] API Request: GET https://
|
|
275
|
+
[mixpeek] API Request: GET https://api.mixpeek.com/v1/health
|
|
276
276
|
[mixpeek] API Response: { status: 200, ... }
|
|
277
277
|
[mixpeek] Health check passed: API responding in 234ms
|
|
278
278
|
```
|
|
@@ -46,24 +46,24 @@ Before you begin, make sure you have:
|
|
|
46
46
|
### Option 1: NPM (Recommended)
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
npm install @mixpeek/prebid
|
|
49
|
+
npm install @mixpeek/prebid
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Then include in your JavaScript:
|
|
53
53
|
|
|
54
54
|
```javascript
|
|
55
|
-
import '@mixpeek/prebid
|
|
55
|
+
import '@mixpeek/prebid'
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
### Option 2: CDN
|
|
59
59
|
|
|
60
60
|
```html
|
|
61
|
-
<script src="https://cdn.jsdelivr.net/npm/@mixpeek/prebid
|
|
61
|
+
<script src="https://cdn.jsdelivr.net/npm/@mixpeek/prebid@latest/dist/mixpeekContextAdapter.js"></script>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
### Option 3: Download
|
|
65
65
|
|
|
66
|
-
Download the latest release from [GitHub releases](https://github.com/mixpeek/prebid
|
|
66
|
+
Download the latest release from [GitHub releases](https://github.com/mixpeek/prebid/releases) and include it in your page:
|
|
67
67
|
|
|
68
68
|
```html
|
|
69
69
|
<script src="/path/to/mixpeekContextAdapter.js"></script>
|
|
@@ -566,7 +566,7 @@ pbjs.setConfig({ mixpeek: {...} }); // Too early
|
|
|
566
566
|
|
|
567
567
|
- **Documentation**: [docs.mixpeek.com](https://docs.mixpeek.com)
|
|
568
568
|
- **Email**: support@mixpeek.com
|
|
569
|
-
- **GitHub Issues**: [github.com/mixpeek/prebid
|
|
569
|
+
- **GitHub Issues**: [github.com/mixpeek/prebid/issues](https://github.com/mixpeek/prebid/issues)
|
|
570
570
|
- **Slack Community**: [Join our Slack](https://mixpeek.com/slack)
|
|
571
571
|
|
|
572
572
|
## Next Steps
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mixpeek/prebid",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Mixpeek for Prebid.js -
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Mixpeek RTD (Real-Time Data) Adapter for Prebid.js - Privacy-first contextual targeting with sub-100ms performance, ad adjacency awareness, and cookie-free bid enrichment",
|
|
5
5
|
"main": "dist/mixpeekContextAdapter.js",
|
|
6
6
|
"module": "src/modules/mixpeekContextAdapter.js",
|
|
7
7
|
"scripts": {
|
|
@@ -37,20 +37,26 @@
|
|
|
37
37
|
],
|
|
38
38
|
"keywords": [
|
|
39
39
|
"prebid",
|
|
40
|
+
"rtd",
|
|
41
|
+
"real-time-data",
|
|
40
42
|
"contextual",
|
|
41
43
|
"advertising",
|
|
44
|
+
"header-bidding",
|
|
45
|
+
"cookie-free",
|
|
46
|
+
"privacy",
|
|
42
47
|
"mixpeek",
|
|
43
48
|
"ai",
|
|
44
49
|
"multimodal",
|
|
45
50
|
"iab",
|
|
46
51
|
"taxonomy",
|
|
47
|
-
"brand-safety"
|
|
52
|
+
"brand-safety",
|
|
53
|
+
"adjacency"
|
|
48
54
|
],
|
|
49
55
|
"author": "Mixpeek",
|
|
50
56
|
"license": "Apache-2.0",
|
|
51
57
|
"repository": {
|
|
52
58
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/mixpeek/prebid.git"
|
|
59
|
+
"url": "git+https://github.com/mixpeek/prebid.git"
|
|
54
60
|
},
|
|
55
61
|
"bugs": {
|
|
56
62
|
"url": "https://github.com/mixpeek/prebid/issues"
|
package/src/api/mixpeekClient.js
CHANGED
|
@@ -35,16 +35,18 @@ class MixpeekClient {
|
|
|
35
35
|
/**
|
|
36
36
|
* Build headers for API request
|
|
37
37
|
* @private
|
|
38
|
+
* @param {boolean} requireNamespace - Whether namespace header is required
|
|
38
39
|
* @returns {object} Headers object
|
|
39
40
|
*/
|
|
40
|
-
_buildHeaders() {
|
|
41
|
+
_buildHeaders(requireNamespace = true) {
|
|
41
42
|
const headers = {
|
|
42
43
|
[HEADERS.CONTENT_TYPE]: 'application/json',
|
|
43
44
|
[HEADERS.AUTHORIZATION]: `Bearer ${this.apiKey}`,
|
|
44
45
|
[HEADERS.USER_AGENT]: USER_AGENT
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
// Add namespace header (required for most endpoints)
|
|
49
|
+
if (this.namespace && requireNamespace) {
|
|
48
50
|
headers[HEADERS.NAMESPACE] = this.namespace
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -135,11 +137,16 @@ class MixpeekClient {
|
|
|
135
137
|
*/
|
|
136
138
|
async createDocument(collectionId, payload) {
|
|
137
139
|
const path = ENDPOINTS.DOCUMENTS.replace('{collectionId}', collectionId)
|
|
138
|
-
|
|
140
|
+
|
|
141
|
+
// Build request payload according to Mixpeek API spec
|
|
139
142
|
const requestPayload = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
collection_id: collectionId,
|
|
144
|
+
...payload.metadata
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add content field if provided
|
|
148
|
+
if (payload.content) {
|
|
149
|
+
requestPayload.content = payload.content
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
return retryWithBackoff(
|
|
@@ -166,52 +173,146 @@ class MixpeekClient {
|
|
|
166
173
|
* Process content with feature extractors
|
|
167
174
|
* @param {string} collectionId - Collection ID
|
|
168
175
|
* @param {object} content - Content to process
|
|
169
|
-
* @param {array} featureExtractors - Feature extractors to use
|
|
170
|
-
* @returns {Promise} Enriched document
|
|
176
|
+
* @param {array} featureExtractors - Feature extractors to use (optional, for future use)
|
|
177
|
+
* @returns {Promise} Enriched document with context data
|
|
171
178
|
*/
|
|
172
179
|
async processContent(collectionId, content, featureExtractors = []) {
|
|
173
180
|
logger.group('Processing content with Mixpeek')
|
|
174
181
|
logger.info('Collection:', collectionId)
|
|
175
|
-
logger.info('
|
|
182
|
+
logger.info('Content URL:', content.url)
|
|
176
183
|
|
|
177
184
|
try {
|
|
178
|
-
//
|
|
179
|
-
const features = featureExtractors.map(extractor => {
|
|
180
|
-
const feature = {
|
|
181
|
-
feature_extractor_id: typeof extractor === 'string' ? extractor : extractor.feature_extractor_id
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Add payload if provided
|
|
185
|
-
if (typeof extractor === 'object' && extractor.payload) {
|
|
186
|
-
feature.payload = extractor.payload
|
|
187
|
-
} else {
|
|
188
|
-
// Build payload from content
|
|
189
|
-
feature.payload = this._buildFeaturePayload(content)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return feature
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// Create document with features
|
|
185
|
+
// Create document in collection
|
|
196
186
|
const document = await this.createDocument(collectionId, {
|
|
197
|
-
|
|
187
|
+
content: content.text || content.description || '',
|
|
198
188
|
metadata: {
|
|
199
189
|
url: content.url,
|
|
200
190
|
title: content.title,
|
|
201
191
|
timestamp: Date.now()
|
|
202
|
-
}
|
|
203
|
-
features
|
|
192
|
+
}
|
|
204
193
|
})
|
|
205
194
|
|
|
206
195
|
logger.info('Document created:', document.document_id)
|
|
196
|
+
|
|
197
|
+
// Build enrichments from content analysis (client-side fallback)
|
|
198
|
+
// In future versions, this will use Mixpeek's taxonomy and classification APIs
|
|
199
|
+
const enrichments = this._buildLocalEnrichments(content)
|
|
200
|
+
|
|
207
201
|
logger.groupEnd()
|
|
208
202
|
|
|
209
|
-
return
|
|
203
|
+
return {
|
|
204
|
+
document_id: document.document_id,
|
|
205
|
+
collection_id: document.collection_id,
|
|
206
|
+
enrichments
|
|
207
|
+
}
|
|
210
208
|
} catch (error) {
|
|
211
209
|
logger.error('Error processing content:', error)
|
|
212
210
|
logger.groupEnd()
|
|
213
|
-
|
|
211
|
+
|
|
212
|
+
// Return fallback enrichments on API error (graceful degradation)
|
|
213
|
+
return {
|
|
214
|
+
document_id: null,
|
|
215
|
+
collection_id: collectionId,
|
|
216
|
+
enrichments: this._buildLocalEnrichments(content)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build local enrichments from content (client-side analysis)
|
|
223
|
+
* This provides basic contextual data when API processing is unavailable
|
|
224
|
+
* @private
|
|
225
|
+
* @param {object} content - Content object
|
|
226
|
+
* @returns {object} Enrichments object
|
|
227
|
+
*/
|
|
228
|
+
_buildLocalEnrichments(content) {
|
|
229
|
+
const enrichments = {}
|
|
230
|
+
|
|
231
|
+
// Extract keywords from content
|
|
232
|
+
if (content.text) {
|
|
233
|
+
enrichments.keywords = this._extractKeywords(content.text)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Analyze sentiment (basic)
|
|
237
|
+
if (content.text) {
|
|
238
|
+
enrichments.sentiment = this._analyzeSentiment(content.text)
|
|
214
239
|
}
|
|
240
|
+
|
|
241
|
+
// Generate content hash as embedding ID
|
|
242
|
+
enrichments.embeddings = [{
|
|
243
|
+
id: `emb_${this._generateContentId(content)}`
|
|
244
|
+
}]
|
|
245
|
+
|
|
246
|
+
return enrichments
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Extract keywords from text (simple implementation)
|
|
251
|
+
* @private
|
|
252
|
+
* @param {string} text - Text content
|
|
253
|
+
* @returns {array} Array of keywords
|
|
254
|
+
*/
|
|
255
|
+
_extractKeywords(text) {
|
|
256
|
+
if (!text) return []
|
|
257
|
+
|
|
258
|
+
// Simple keyword extraction: common important words
|
|
259
|
+
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'just', 'don', 'now', 'and', 'but', 'or', 'if', 'this', 'that', 'these', 'those', 'it', 'its'])
|
|
260
|
+
|
|
261
|
+
const words = text.toLowerCase()
|
|
262
|
+
.replace(/[^\w\s]/g, ' ')
|
|
263
|
+
.split(/\s+/)
|
|
264
|
+
.filter(word => word.length > 3 && !stopWords.has(word))
|
|
265
|
+
|
|
266
|
+
// Count word frequency
|
|
267
|
+
const wordCount = {}
|
|
268
|
+
words.forEach(word => {
|
|
269
|
+
wordCount[word] = (wordCount[word] || 0) + 1
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Return top 10 keywords by frequency
|
|
273
|
+
return Object.entries(wordCount)
|
|
274
|
+
.sort((a, b) => b[1] - a[1])
|
|
275
|
+
.slice(0, 10)
|
|
276
|
+
.map(([word]) => word)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Basic sentiment analysis
|
|
281
|
+
* @private
|
|
282
|
+
* @param {string} text - Text content
|
|
283
|
+
* @returns {object} Sentiment result
|
|
284
|
+
*/
|
|
285
|
+
_analyzeSentiment(text) {
|
|
286
|
+
if (!text) return { label: 'neutral', score: 0.5 }
|
|
287
|
+
|
|
288
|
+
const positiveWords = ['good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', 'best', 'love', 'happy', 'positive', 'success', 'win', 'awesome', 'brilliant', 'perfect', 'beautiful', 'enjoy', 'exciting']
|
|
289
|
+
const negativeWords = ['bad', 'terrible', 'awful', 'horrible', 'worst', 'hate', 'sad', 'negative', 'fail', 'loss', 'poor', 'ugly', 'boring', 'disappointing', 'wrong', 'problem', 'issue', 'error']
|
|
290
|
+
|
|
291
|
+
const lowerText = text.toLowerCase()
|
|
292
|
+
let positiveCount = 0
|
|
293
|
+
let negativeCount = 0
|
|
294
|
+
|
|
295
|
+
positiveWords.forEach(word => {
|
|
296
|
+
const regex = new RegExp(`\\b${word}\\b`, 'gi')
|
|
297
|
+
const matches = lowerText.match(regex)
|
|
298
|
+
if (matches) positiveCount += matches.length
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
negativeWords.forEach(word => {
|
|
302
|
+
const regex = new RegExp(`\\b${word}\\b`, 'gi')
|
|
303
|
+
const matches = lowerText.match(regex)
|
|
304
|
+
if (matches) negativeCount += matches.length
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const total = positiveCount + negativeCount
|
|
308
|
+
if (total === 0) return { label: 'neutral', score: 0.5 }
|
|
309
|
+
|
|
310
|
+
const score = positiveCount / total
|
|
311
|
+
let label = 'neutral'
|
|
312
|
+
if (score > 0.6) label = 'positive'
|
|
313
|
+
else if (score < 0.4) label = 'negative'
|
|
314
|
+
|
|
315
|
+
return { label, score }
|
|
215
316
|
}
|
|
216
317
|
|
|
217
318
|
/**
|
package/src/config/constants.js
CHANGED
|
@@ -17,7 +17,7 @@ export const DEFAULT_API_ENDPOINT = typeof process !== 'undefined' && process.en
|
|
|
17
17
|
// Alternative endpoints
|
|
18
18
|
export const API_ENDPOINTS = {
|
|
19
19
|
PRODUCTION: 'https://api.mixpeek.com',
|
|
20
|
-
DEVELOPMENT: 'https://
|
|
20
|
+
DEVELOPMENT: 'https://api.mixpeek.com',
|
|
21
21
|
LOCAL: 'http://localhost:8000'
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -43,14 +43,19 @@ export const CONTENT_MODES = {
|
|
|
43
43
|
IMAGE: 'image'
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Feature Extractors
|
|
46
|
+
// Feature Extractors (actual Mixpeek API extractors)
|
|
47
47
|
export const FEATURE_EXTRACTORS = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// Actual Mixpeek extractors
|
|
49
|
+
TEXT: 'text_extractor_v1',
|
|
50
|
+
SENTIMENT: 'sentiment_classifier_v1',
|
|
51
|
+
IMAGE: 'image_extractor_v1',
|
|
52
|
+
MULTIMODAL: 'multimodal_extractor_v1',
|
|
53
|
+
// Legacy aliases for backwards compatibility
|
|
54
|
+
TAXONOMY: 'text_extractor_v1',
|
|
55
|
+
BRAND_SAFETY: 'sentiment_classifier_v1',
|
|
56
|
+
KEYWORDS: 'text_extractor_v1',
|
|
57
|
+
CLUSTERING: 'text_extractor_v1',
|
|
58
|
+
EMBEDDING: 'text_extractor_v1'
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// Targeting Key Prefixes
|
|
@@ -452,10 +452,10 @@ class MixpeekContextAdapter {
|
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
// Extract taxonomies
|
|
455
|
+
// Extract taxonomies (if available from Mixpeek classification)
|
|
456
456
|
if (document.enrichments && document.enrichments.taxonomies) {
|
|
457
457
|
const taxonomies = document.enrichments.taxonomies
|
|
458
|
-
|
|
458
|
+
|
|
459
459
|
if (taxonomies.length > 0) {
|
|
460
460
|
const primaryTaxonomy = taxonomies[0]
|
|
461
461
|
context.taxonomy = {
|
|
@@ -470,9 +470,18 @@ class MixpeekContextAdapter {
|
|
|
470
470
|
|
|
471
471
|
// Extract other enrichments
|
|
472
472
|
if (document.enrichments) {
|
|
473
|
-
// Brand safety
|
|
473
|
+
// Brand safety (use sentiment as proxy for now)
|
|
474
474
|
if (document.enrichments.brand_safety) {
|
|
475
475
|
context.brandSafety = document.enrichments.brand_safety
|
|
476
|
+
} else if (document.enrichments.sentiment) {
|
|
477
|
+
// Use positive sentiment as a proxy for brand safety
|
|
478
|
+
const sentimentScore = typeof document.enrichments.sentiment === 'object'
|
|
479
|
+
? document.enrichments.sentiment.score
|
|
480
|
+
: 0.5
|
|
481
|
+
context.brandSafety = {
|
|
482
|
+
score: sentimentScore > 0.5 ? 0.8 + (sentimentScore - 0.5) * 0.4 : 0.5 + sentimentScore * 0.6,
|
|
483
|
+
level: sentimentScore > 0.6 ? 'safe' : sentimentScore < 0.4 ? 'caution' : 'neutral'
|
|
484
|
+
}
|
|
476
485
|
}
|
|
477
486
|
|
|
478
487
|
// Keywords
|
|
@@ -491,9 +500,52 @@ class MixpeekContextAdapter {
|
|
|
491
500
|
}
|
|
492
501
|
}
|
|
493
502
|
|
|
503
|
+
// Build a taxonomy-like structure from keywords if no taxonomy available
|
|
504
|
+
if (!context.taxonomy && context.keywords && context.keywords.length > 0) {
|
|
505
|
+
context.taxonomy = {
|
|
506
|
+
label: this._inferCategoryFromKeywords(context.keywords),
|
|
507
|
+
nodeId: `kw_${context.keywords[0]}`,
|
|
508
|
+
path: ['Content', this._inferCategoryFromKeywords(context.keywords)],
|
|
509
|
+
score: 0.7 // Moderate confidence for keyword-based classification
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
494
513
|
return context
|
|
495
514
|
}
|
|
496
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Infer a category from keywords (simple heuristic)
|
|
518
|
+
* @private
|
|
519
|
+
* @param {array} keywords - Extracted keywords
|
|
520
|
+
* @returns {string} Inferred category
|
|
521
|
+
*/
|
|
522
|
+
_inferCategoryFromKeywords(keywords) {
|
|
523
|
+
const categoryKeywords = {
|
|
524
|
+
'Technology': ['technology', 'software', 'computer', 'digital', 'tech', 'programming', 'code', 'developer', 'app', 'mobile', 'phone', 'smartphone'],
|
|
525
|
+
'Business': ['business', 'company', 'market', 'finance', 'investment', 'stock', 'economy', 'corporate', 'startup', 'entrepreneur'],
|
|
526
|
+
'Sports': ['sports', 'game', 'team', 'player', 'football', 'basketball', 'soccer', 'baseball', 'tennis', 'golf', 'match'],
|
|
527
|
+
'Entertainment': ['movie', 'film', 'music', 'celebrity', 'actor', 'singer', 'show', 'concert', 'entertainment', 'tv', 'television'],
|
|
528
|
+
'Health': ['health', 'medical', 'doctor', 'hospital', 'medicine', 'disease', 'fitness', 'wellness', 'diet', 'nutrition'],
|
|
529
|
+
'News': ['news', 'breaking', 'report', 'politics', 'government', 'election', 'policy', 'world', 'international'],
|
|
530
|
+
'Science': ['science', 'research', 'study', 'experiment', 'discovery', 'scientist', 'physics', 'chemistry', 'biology'],
|
|
531
|
+
'Automotive': ['car', 'vehicle', 'auto', 'automotive', 'driving', 'electric', 'engine', 'motor', 'truck'],
|
|
532
|
+
'Travel': ['travel', 'vacation', 'hotel', 'flight', 'destination', 'tourism', 'trip', 'adventure'],
|
|
533
|
+
'Food': ['food', 'recipe', 'cooking', 'restaurant', 'cuisine', 'chef', 'meal', 'dinner', 'lunch']
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const lowerKeywords = keywords.map(k => k.toLowerCase())
|
|
537
|
+
|
|
538
|
+
for (const [category, catKeywords] of Object.entries(categoryKeywords)) {
|
|
539
|
+
for (const keyword of lowerKeywords) {
|
|
540
|
+
if (catKeywords.includes(keyword)) {
|
|
541
|
+
return category
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return 'General'
|
|
547
|
+
}
|
|
548
|
+
|
|
497
549
|
/**
|
|
498
550
|
* Inject targeting keys into ad units
|
|
499
551
|
* @private
|