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:
blomios
2026-10-02 11:13:34 +02:00
parent 9e80655fd9
commit ce11ecc757
19 changed files with 780 additions and 13 deletions

View File

@ -10,7 +10,7 @@
<title>nodecast-tv | IPTV Player</title>
<meta name="description" content="Stream live TV, movies, and series with EPG support">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="css/main.css?v=2">
<link rel="stylesheet" href="css/main.css?v=3">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@800&display=swap"
@ -466,6 +466,36 @@
<span class="setting-hint">Turn off to show all channels in a single list. Stream variants remain grouped by channel.</span>
</div>
</div>
<div class="channel-matching-settings">
<h4>Channel matching</h4>
<p class="setting-hint">Choose how Nodecast TV identifies matching streams and languages.</p>
<div class="matching-mode-options">
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="algo" checked>
<span><strong>Standard (Recommended)</strong><small>Use the built-in matching system. Fast and no download required.</small></span>
</label>
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="laya">
<span><strong>Local AI only</strong><small>Use the downloaded AI model instead of the built-in matching system.</small></span>
</label>
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="hybrid">
<span><strong>Enhanced</strong><small>Combine the local AI model with the built-in matching system for more evidence.</small></span>
</label>
</div>
<div class="laya-model-panel" id="laya-model-panel">
<div class="laya-model-heading">
<div><strong>Local AI model · Laya</strong><div id="laya-model-status" class="setting-hint" role="status">Checking status…</div></div>
<span id="laya-model-size" class="setting-hint"></span>
</div>
<div id="laya-model-message" class="setting-hint"></div>
<div class="laya-model-actions">
<button type="button" class="btn btn-primary" id="laya-download-model">Download model</button>
<button type="button" class="btn btn-ghost" id="laya-remove-model">Remove downloaded model</button>
</div>
<p class="setting-hint">The model runs on this server. Channel names and metadata are not sent to an AI service.</p>
</div>
</div>
</div>
<div class="settings-section">
@ -1137,11 +1167,12 @@
<!-- JavaScript -->
<script src="/js/icons.js"></script>
<script src="/js/api.js?v=2"></script>
<script src="/js/components/VideoPlayer.js?v=4"></script>
<script src="/js/api.js?v=3"></script>
<script src="/js/components/VideoPlayer.js?v=5"></script>
<script src="/js/services/channelLanguageDetection.js?v=1"></script>
<script src="/js/services/channelVariantGrouping.js?v=3"></script>
<script src="/js/components/ChannelList.js?v=6"></script>
<script src="/js/services/channelLanguageFusion.js?v=1"></script>
<script src="/js/services/channelVariantGrouping.js?v=4"></script>
<script src="/js/components/ChannelList.js?v=7"></script>
<script src="/js/components/SourceManager.js?v=2"></script>
<script src="/js/components/EpgGuide.js?v=2"></script>
<script src="/js/pages/HomePage.js?v=2"></script>
@ -1149,7 +1180,7 @@
<script src="/js/pages/Guide.js?v=2"></script>
<script src="/js/pages/MoviesPage.js?v=4"></script>
<script src="/js/pages/SeriesPage.js?v=1"></script>
<script src="/js/pages/Settings.js?v=3"></script>
<script src="/js/pages/Settings.js?v=4"></script>
<script src="/js/pages/WatchPage.js?v=1"></script>
<script src="/js/app.js?v=4"></script>
<script>