feat: use Laya as a weighted signal in live stream grouping (#16)
Introduce a liveGroupingScoring engine that combines existing membership signals (normalized name, brand, number, metadata tokens, language, category/source when relevant) with a bounded Laya contribution: Laya raises or lowers confidence instead of acting as a final judge, with guard-rails against obvious contradictions. Sanitize Laya batch identity inputs in layaService and wire the weighted scoring into the live projection service. Weights are not exposed in the Nodecast UI. Cover signal combination, bounds and contradiction guard-rails with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -103,6 +103,22 @@ function initialDownloadState() {
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeIdentityText(value, maxLength) {
|
||||
return String(value || '')
|
||||
.replace(/https?:\/\/\S+/gi, '[redacted]')
|
||||
.replace(/\b(?:password|passwd|token|username|user)\s*[=:]\s*\S+/gi, '[redacted]')
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
|
||||
function sanitizeIdentityDescriptor(value) {
|
||||
return {
|
||||
name: sanitizeIdentityText(value?.name, 300),
|
||||
group: sanitizeIdentityText(value?.group, 200),
|
||||
source: sanitizeIdentityText(value?.source, 120),
|
||||
tvgId: sanitizeIdentityText(value?.tvgId, 160)
|
||||
};
|
||||
}
|
||||
|
||||
class LayaService {
|
||||
constructor(options = {}) {
|
||||
this.baseUrl = options.baseUrl || process.env.LAYA_BASE_URL || 'http://laya:8000';
|
||||
@ -300,6 +316,41 @@ class LayaService {
|
||||
}
|
||||
return { runtimeAvailable: true, model, results };
|
||||
}
|
||||
|
||||
async identityHintBatch(pairs) {
|
||||
if (!Array.isArray(pairs) || pairs.length === 0 || pairs.length > MAX_BATCH_ITEMS) {
|
||||
throw new LayaRequestError(`Identity hint batch must contain 1 to ${MAX_BATCH_ITEMS} pairs`, 400);
|
||||
}
|
||||
const response = await this.request('/v1/systemone/batch', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
states: pairs.map(pair => ({
|
||||
left: sanitizeIdentityDescriptor(pair.left),
|
||||
right: sanitizeIdentityDescriptor(pair.right)
|
||||
})),
|
||||
model: 'multilingual',
|
||||
questions: {
|
||||
sameIdentity: {
|
||||
type: 'noul',
|
||||
instructions: 'Do these two TV channel descriptors identify the same numbered channel?'
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
if (!response || !Array.isArray(response.results) || response.results.length !== pairs.length) {
|
||||
throw new LayaRequestError('Laya runtime returned an invalid identity batch response', 502);
|
||||
}
|
||||
return pairs.map((pair, index) => {
|
||||
const answer = response.results[index]?.answers?.sameIdentity || {};
|
||||
return {
|
||||
id: pair.id,
|
||||
sameIdentityProbability: Number(answer.noul) || 0,
|
||||
confidence: Number(answer.answer_confidence ?? answer.confidence) || 0,
|
||||
family: answer.family || null,
|
||||
number: answer.number || null
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -310,5 +361,6 @@ module.exports = {
|
||||
clearDirectoryContents,
|
||||
directorySize,
|
||||
initialDownloadState,
|
||||
sanitizeIdentityDescriptor,
|
||||
validateBatchPayload
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user