feat: detect variant language with confidence scoring and player language selector (#10)
Add a channelLanguageDetection service that scores language evidence per IPTV variant (country/language prefixes in name and group, source metadata, original title words) and picks the highest-scored language. Integrate detected languages into channel variant grouping and add a language selector to the live player to switch to a variant of the logical channel in the desired language, keeping raw streams non-destructive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
66
test/channelLanguageDetection.test.js
Normal file
66
test/channelLanguageDetection.test.js
Normal file
@ -0,0 +1,66 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const detection = require('../public/js/services/channelLanguageDetection.js');
|
||||
const grouping = require('../public/js/services/channelVariantGrouping.js');
|
||||
|
||||
const channel = (id, name, extra = {}) => ({
|
||||
id, streamId: id, sourceId: 1, name, groupTitle: 'Sports', ...extra
|
||||
});
|
||||
|
||||
test('scores explicit French prefixes with strong confidence', () => {
|
||||
for (const name of ['|FR| BEIN SPORTS 1 UHD', 'FR-CAR| bein sports 1']) {
|
||||
const result = detection.detectLanguage(channel(name, name));
|
||||
assert.equal(result.language.code, 'fr');
|
||||
assert.match(result.language.status, /medium|high/);
|
||||
assert.ok(result.languageScores.fr >= 75);
|
||||
assert.ok(result.languageEvidence.some(item => item.type === 'name-prefix'));
|
||||
}
|
||||
});
|
||||
|
||||
test('maps UK group evidence to English with medium confidence', () => {
|
||||
const result = detection.detectLanguage(channel('1', 'Sports One', { groupTitle: 'UK Sports' }));
|
||||
assert.equal(result.language.code, 'en');
|
||||
assert.equal(result.language.status, 'medium');
|
||||
});
|
||||
|
||||
test('uses a France source as weak French evidence', () => {
|
||||
const result = detection.detectLanguage(channel('1', 'Sports One'), { sourceName: 'France IPTV' });
|
||||
assert.equal(result.language.code, 'fr');
|
||||
assert.equal(result.language.status, 'low');
|
||||
});
|
||||
|
||||
test('does not force multilingual BE to a language', () => {
|
||||
const result = detection.detectLanguage(channel('1', 'BE Sports 1'));
|
||||
assert.equal(result.language.code, 'unknown');
|
||||
});
|
||||
|
||||
test('classifies contradictory close evidence as unknown', () => {
|
||||
const result = detection.detectLanguage(channel('1', 'French English Sports'));
|
||||
assert.equal(result.language.code, 'unknown');
|
||||
assert.equal(result.language.status, 'unknown');
|
||||
});
|
||||
|
||||
test('global and per-channel language preferences resolve tied variants', () => {
|
||||
const variants = [channel('fr', '|FR| Sports One HD'), channel('en', '|UK| Sports One HD')];
|
||||
const globalEnglish = grouping.groupChannels(variants, { preferredLanguage: 'en' })[0];
|
||||
assert.equal(globalEnglish.preferredVariant.id, 'en');
|
||||
|
||||
const perChannel = grouping.groupChannels(variants, {
|
||||
preferredLanguage: 'en',
|
||||
languagePreferences: new Map([['sports one', 'fr']])
|
||||
})[0];
|
||||
assert.equal(perChannel.preferredVariant.id, 'fr');
|
||||
});
|
||||
|
||||
test('explicit language preference beats quality and hidden variants are ignored', () => {
|
||||
const frenchSd = channel('fr', '|FR| Sports One SD');
|
||||
const english4k = channel('en', '|UK| Sports One 4K');
|
||||
const preferredFrench = grouping.groupChannels([frenchSd, english4k], { preferredLanguage: 'fr' })[0];
|
||||
assert.equal(preferredFrench.preferredVariant.id, 'fr');
|
||||
|
||||
const hiddenFrench = grouping.groupChannels([frenchSd, english4k], {
|
||||
preferredLanguage: 'fr',
|
||||
isHidden: variant => variant.id === 'fr'
|
||||
})[0];
|
||||
assert.equal(hiddenFrench.preferredVariant.id, 'en');
|
||||
});
|
||||
Reference in New Issue
Block a user