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:
@ -31,6 +31,9 @@ class ChannelList {
|
||||
this.channelLanguagePreferencesKey = 'nodecast_tv_channel_language_preferences';
|
||||
this.preferredLanguage = this.loadPreferredLanguage();
|
||||
this.channelLanguagePreferences = this.loadChannelLanguagePreferences();
|
||||
this.layaLanguageResults = new Map();
|
||||
this.languageDetectionNotice = '';
|
||||
this._languageDetectionRequest = 0;
|
||||
|
||||
this.loadCollapsedState();
|
||||
this.init();
|
||||
@ -328,6 +331,52 @@ class ChannelList {
|
||||
if (this.channels.length > 0) this.render();
|
||||
}
|
||||
|
||||
setLanguageDetectionMode(mode) {
|
||||
const safeMode = ['algo', 'laya', 'hybrid'].includes(mode) ? mode : 'algo';
|
||||
this.layaLanguageResults.clear();
|
||||
this.languageDetectionNotice = '';
|
||||
if (this.channels.length > 0) {
|
||||
this.render();
|
||||
void this.refreshLayaLanguages(safeMode);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshLayaLanguages(mode = window.app?.player?.settings?.languageDetectionMode || 'algo') {
|
||||
const requestId = ++this._languageDetectionRequest;
|
||||
if (mode === 'algo') {
|
||||
this.layaLanguageResults.clear();
|
||||
this.languageDetectionNotice = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptors = this.channels.map(variant => {
|
||||
const algo = ChannelLanguageDetection.detectLanguage(variant, { sourceName: variant.sourceName });
|
||||
return ChannelLanguageFusion.buildLayaDescriptor(
|
||||
variant,
|
||||
ChannelVariantGrouping.getVariantKey(variant),
|
||||
algo
|
||||
);
|
||||
});
|
||||
const results = new Map();
|
||||
try {
|
||||
for (let index = 0; index < descriptors.length; index += 64) {
|
||||
const response = await API.laya.detectLanguages(descriptors.slice(index, index + 64));
|
||||
(response.results || []).forEach(result => results.set(String(result.id), result));
|
||||
}
|
||||
if (requestId !== this._languageDetectionRequest) return;
|
||||
this.layaLanguageResults = results;
|
||||
this.languageDetectionNotice = '';
|
||||
} catch (err) {
|
||||
if (requestId !== this._languageDetectionRequest) return;
|
||||
this.layaLanguageResults.clear();
|
||||
this.languageDetectionNotice = mode === 'hybrid'
|
||||
? 'Local AI is unavailable; using standard channel matching.'
|
||||
: 'Local AI is unavailable. Download or start the model in Settings.';
|
||||
console.warn('[ChannelList] Local AI language detection unavailable:', err.message);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
@ -356,6 +405,8 @@ class ChannelList {
|
||||
selectedVariants: this.selectedVariants,
|
||||
preferredLanguage: this.preferredLanguage,
|
||||
languagePreferences: this.channelLanguagePreferences,
|
||||
languageMode: window.app?.player?.settings?.languageDetectionMode || 'algo',
|
||||
languageResults: this.layaLanguageResults,
|
||||
isFavorite: channel => this.isFavorite(channel.sourceId, channel.id),
|
||||
isHidden: channel => this.isHidden('channel', channel.sourceId, channel.streamId || channel.id)
|
||||
});
|
||||
@ -364,6 +415,13 @@ class ChannelList {
|
||||
this.currentBatch = 0;
|
||||
this.batchSize = 100; // Number of groups to render per batch (increased to handle many hidden groups)
|
||||
this.container.innerHTML = ''; // Clear container
|
||||
if (this.languageDetectionNotice) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'laya-language-notice';
|
||||
notice.setAttribute('role', 'status');
|
||||
notice.textContent = this.languageDetectionNotice;
|
||||
this.container.appendChild(notice);
|
||||
}
|
||||
|
||||
// Filter and Group channels
|
||||
const groupedChannels = {};
|
||||
@ -837,6 +895,7 @@ class ChannelList {
|
||||
]);
|
||||
|
||||
this.render();
|
||||
void this.refreshLayaLanguages();
|
||||
} catch (err) {
|
||||
console.error('Error loading channels:', err);
|
||||
this.container.innerHTML = `<div class="empty-state"><p>Error loading channels</p><p class="hint">${err.message}</p></div>`;
|
||||
@ -872,6 +931,7 @@ class ChannelList {
|
||||
this.loadFavorites()
|
||||
]);
|
||||
this.render();
|
||||
void this.refreshLayaLanguages();
|
||||
} catch (err) {
|
||||
console.error('Error loading all channels:', err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user