feat: add optional Laya engine mode for grouping and language detection (#12)
Add a languageDetectionMode setting (algo | laya | laya+algo) exposed in Settings with three engine choices. When Laya is enabled, a laya service (Dockerfile.laya, optional compose profile, models stored outside the repo under ./data/models/laya) can be started, with model download and deletion from the UI via /laya routes to manage disk space. Add a channelLanguageFusion service combining Laya results with the existing algorithm, and wire detection mode through grouping, channel list and player. Cover engine selection, fusion and laya service behavior with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
83
public/js/services/channelLanguageFusion.js
Normal file
83
public/js/services/channelLanguageFusion.js
Normal file
@ -0,0 +1,83 @@
|
||||
(function (root, factory) {
|
||||
const service = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = service;
|
||||
if (root) root.ChannelLanguageFusion = service;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||
const LABELS = { fr: 'French', en: 'English', es: 'Spanish', unknown: 'Unknown' };
|
||||
|
||||
function normalizeConfidence(value) {
|
||||
const number = Number(value) || 0;
|
||||
return Math.max(0, Math.min(100, Math.round(number <= 1 ? number * 100 : number)));
|
||||
}
|
||||
|
||||
function layaDetection(result) {
|
||||
const rawCode = result?.language?.code || result?.language || 'unknown';
|
||||
const code = ['fr', 'en', 'es'].includes(rawCode) ? rawCode : 'unknown';
|
||||
const confidence = normalizeConfidence(result?.language?.confidence ?? result?.confidence);
|
||||
return {
|
||||
language: {
|
||||
code,
|
||||
label: LABELS[code],
|
||||
confidence: code === 'unknown' ? 0 : confidence,
|
||||
status: code === 'unknown' ? 'unknown' : confidence >= 75 ? 'high' : confidence >= 60 ? 'medium' : 'low'
|
||||
},
|
||||
languageScores: code === 'unknown' ? {} : { [code]: confidence },
|
||||
languageEvidence: code === 'unknown' ? [] : [{ code, weight: confidence, type: 'laya', value: 'Local AI model' }]
|
||||
};
|
||||
}
|
||||
|
||||
function mergeHybrid(algo, result) {
|
||||
if (!result) return algo;
|
||||
const laya = layaDetection(result);
|
||||
if (laya.language.code === 'unknown') return {
|
||||
...algo,
|
||||
languageEvidence: [...algo.languageEvidence, ...laya.languageEvidence]
|
||||
};
|
||||
const combined = { ...algo.languageScores };
|
||||
combined[laya.language.code] = (combined[laya.language.code] || 0) + laya.language.confidence;
|
||||
const total = Object.values(combined).reduce((sum, value) => sum + value, 0);
|
||||
const scores = Object.fromEntries(Object.entries(combined).map(([code, value]) => [code, Math.round(value / total * 100)]));
|
||||
const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
||||
const [code, confidence] = ranked[0];
|
||||
const margin = confidence - (ranked[1]?.[1] || 0);
|
||||
const certain = confidence >= 45 && margin >= 10;
|
||||
return {
|
||||
language: certain ? {
|
||||
code,
|
||||
label: LABELS[code] || code.toUpperCase(),
|
||||
confidence,
|
||||
status: confidence >= 75 ? 'high' : confidence >= 60 ? 'medium' : 'low'
|
||||
} : { code: 'unknown', label: 'Unknown', confidence: 0, status: 'unknown' },
|
||||
languageScores: scores,
|
||||
languageEvidence: [...algo.languageEvidence, ...laya.languageEvidence]
|
||||
};
|
||||
}
|
||||
|
||||
function applyLanguageMode(algo, result, mode) {
|
||||
if (mode === 'algo') return algo;
|
||||
if (mode === 'hybrid') return mergeHybrid(algo, result);
|
||||
return result ? layaDetection(result) : {
|
||||
language: { code: 'unknown', label: 'Unknown', confidence: 0, status: 'unknown' },
|
||||
languageScores: {},
|
||||
languageEvidence: []
|
||||
};
|
||||
}
|
||||
|
||||
function buildLayaDescriptor(variant, variantKey, algo) {
|
||||
const fields = [
|
||||
['sourceId', variant.sourceId],
|
||||
['name', variant.name],
|
||||
['groupTitle', variant.groupTitle],
|
||||
['sourceName', variant.sourceName],
|
||||
['tvgId', variant.tvgId],
|
||||
['algorithmScores', JSON.stringify(algo.languageScores || {})]
|
||||
];
|
||||
return {
|
||||
id: variantKey,
|
||||
text: fields.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => `${key}: ${value}`).join('\n')
|
||||
};
|
||||
}
|
||||
|
||||
return { applyLanguageMode, buildLayaDescriptor, layaDetection, mergeHybrid, normalizeConfidence };
|
||||
});
|
||||
@ -2,10 +2,13 @@
|
||||
const detector = typeof module === 'object' && module.exports
|
||||
? require('./channelLanguageDetection.js')
|
||||
: root.ChannelLanguageDetection;
|
||||
const service = factory(detector);
|
||||
const fusion = typeof module === 'object' && module.exports
|
||||
? require('./channelLanguageFusion.js')
|
||||
: root.ChannelLanguageFusion;
|
||||
const service = factory(detector, fusion);
|
||||
if (typeof module === 'object' && module.exports) module.exports = service;
|
||||
if (root) root.ChannelVariantGrouping = service;
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function (languageDetection) {
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function (languageDetection, languageFusion) {
|
||||
const QUALITY_TOKENS = new Set([
|
||||
'4k', 'uhd', 'fhd', 'fullhd', 'hd', 'sd', 'hevc', 'h265', 'h264',
|
||||
'1080p', '1080i', '720p', '576p', '480p'
|
||||
@ -96,10 +99,14 @@
|
||||
});
|
||||
|
||||
return Array.from(buckets, ([fingerprint, rawVariants]) => {
|
||||
const variants = rawVariants.map(variant => ({
|
||||
...variant,
|
||||
...languageDetection.detectLanguage(variant, { sourceName: variant.sourceName })
|
||||
}));
|
||||
const variants = rawVariants.map(variant => {
|
||||
const algo = languageDetection.detectLanguage(variant, { sourceName: variant.sourceName });
|
||||
const result = options.languageResults?.get(getVariantKey(variant));
|
||||
return {
|
||||
...variant,
|
||||
...languageFusion.applyLanguageMode(algo, result, options.languageMode || 'algo')
|
||||
};
|
||||
});
|
||||
const visibleVariants = variants.filter(variant => !isHidden(variant));
|
||||
const selectableVariants = showHidden ? variants : visibleVariants;
|
||||
const languageCounts = selectableVariants.reduce((counts, variant) => {
|
||||
|
||||
Reference in New Issue
Block a user