diff --git a/assets/css/debug.css b/assets/css/debug.css deleted file mode 100644 index e69de29..0000000 diff --git a/assets/css/shortcodes.css b/assets/css/shortcodes.css deleted file mode 100644 index e69de29..0000000 diff --git a/assets/js/contribution-calendar.js b/assets/js/contribution-calendar.js deleted file mode 100644 index 4b2265b..0000000 --- a/assets/js/contribution-calendar.js +++ /dev/null @@ -1,140 +0,0 @@ -// ✅ Constants organization -const CALENDAR_CONFIG = { - TOOLTIP_OFFSET: 10, - SCROLL_DEBOUNCE_MS: 16, // 60fps for smooth scrolling - CELL_SELECTOR: '.calendar-cell', - CONTAINER_SELECTOR: '.calendar-container', - MONTHS_SELECTOR: '.calendar-months' -}; - -const TOOLTIP_STYLES = { - display: 'none', - position: 'absolute', - zIndex: '1000', - pointerEvents: 'none' -}; - -// ✅ Utility functions with error handling -const createTooltip = () => { - const tooltip = document.createElement('div'); - tooltip.className = 'cell-tooltip'; - Object.assign(tooltip.style, TOOLTIP_STYLES); - document.body.appendChild(tooltip); - return tooltip; -}; - -const formatContributionDate = date => { - try { - return new Date(date).toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }); - } catch (error) { - console.error('Date formatting failed:', error); - return 'Invalid date'; - } -}; - -const createTooltipContent = (date, count) => { - const formattedDate = formatContributionDate(date); - const plural = count !== '1' ? 's' : ''; - return `${formattedDate}
${count} contribution${plural}`; -}; - -// ✅ Performance helper - debounced scroll -const debounce = (fn, ms) => { - let timer; - return (...args) => { - clearTimeout(timer); - timer = setTimeout(() => fn.apply(this, args), ms); - }; -}; - -// ✅ Tooltip position with boundary checking -const positionTooltip = (tooltip, x, y) => { - const { innerWidth, innerHeight } = window; - const rect = tooltip.getBoundingClientRect(); - - const left = Math.min(x + CALENDAR_CONFIG.TOOLTIP_OFFSET, innerWidth - rect.width - 10); - const top = Math.min(y + CALENDAR_CONFIG.TOOLTIP_OFFSET, innerHeight - rect.height - 10); - - Object.assign(tooltip.style, { - left: `${Math.max(10, left)}px`, - top: `${Math.max(10, top)}px` - }); -}; - -// ✅ Main calendar functionality with error boundaries -const initializeCalendar = () => { - try { - const calendarCells = document.querySelectorAll(CALENDAR_CONFIG.CELL_SELECTOR); - if (!calendarCells.length) { - console.warn('No calendar cells found'); - return; - } - - const tooltip = createTooltip(); - - // ✅ Event delegation for better performance - document.addEventListener('mouseover', e => { - const cell = e.target.closest(CALENDAR_CONFIG.CELL_SELECTOR); - if (!cell) return; - - const { date, count } = cell.dataset; - if (!date || count === undefined) return; - - tooltip.innerHTML = createTooltipContent(date, count); - tooltip.style.display = 'block'; - positionTooltip(tooltip, e.pageX, e.pageY); - }); - - document.addEventListener('mousemove', e => { - const cell = e.target.closest(CALENDAR_CONFIG.CELL_SELECTOR); - if (!cell || tooltip.style.display === 'none') return; - - positionTooltip(tooltip, e.pageX, e.pageY); - }); - - document.addEventListener('mouseout', e => { - const cell = e.target.closest(CALENDAR_CONFIG.CELL_SELECTOR); - if (!cell) return; - - tooltip.style.display = 'none'; - }); - - initializeScrollSync(); - - } catch (error) { - console.error('Calendar initialization failed:', error); - } -}; - -// ✅ Scroll synchronization with performance optimization -const initializeScrollSync = () => { - const calendarContainer = document.querySelector(CALENDAR_CONFIG.CONTAINER_SELECTOR); - if (!calendarContainer) return; - - const monthsContainer = calendarContainer.querySelector(CALENDAR_CONFIG.MONTHS_SELECTOR); - if (!monthsContainer) return; - - const handleScroll = debounce(() => { - const { scrollLeft } = calendarContainer; - monthsContainer.style.transform = `translateX(${scrollLeft}px)`; - }, CALENDAR_CONFIG.SCROLL_DEBOUNCE_MS); - - calendarContainer.addEventListener('scroll', handleScroll, { passive: true }); -}; - -// ✅ DOM ready with modern approach -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeCalendar); -} else { - initializeCalendar(); -} - -// ✅ Export for potential module usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = { initializeCalendar, formatContributionDate }; -} diff --git a/assets/js/fuzzy-search.js b/assets/js/fuzzy-search.js index 37a5bba..1ae79de 100644 --- a/assets/js/fuzzy-search.js +++ b/assets/js/fuzzy-search.js @@ -1,4 +1,4 @@ -(function() { +(function () { 'use strict'; function FuzzySearch(options) { @@ -6,111 +6,90 @@ this.maxResults = (options && options.maxResults) || 10; this.searchData = []; this.searchIndex = null; - this.init(); + this.loadPromise = null; + this.setupSearchHandlers(); } - FuzzySearch.prototype.init = function() { - var self = this; - this.loadSearchData().then(function() { - self.setupSearchHandlers(); - }); - }; - - FuzzySearch.prototype.loadSearchData = function() { - var self = this; - // Use relative URL - this will work with any baseURL including subpaths - var indexUrl = '/index.json'; - - // For sites with subpath (like GitHub Pages), get the full path from current location - var currentPath = window.location.pathname; - var basePath = ''; - - // If we're in a subpath (not root), extract the base path + // Compute the index URL once, supporting subpath deployments (e.g. GitHub Pages). + FuzzySearch.prototype.getIndexUrl = function () { + const currentPath = window.location.pathname; + let basePath = ''; if (currentPath !== '/' && currentPath.indexOf('/') === 0) { - var pathParts = currentPath.split('/').filter(function(part) { return part.length > 0; }); - // For GitHub Pages format like /PKB-theme/, the first part is usually the repo name + const pathParts = currentPath.split('/').filter((part) => part.length > 0); if (pathParts.length > 0 && currentPath.startsWith('/' + pathParts[0] + '/')) { basePath = '/' + pathParts[0]; } } - - indexUrl = basePath + '/index.json'; - - return fetch(indexUrl) - .then(function(response) { - if (response.ok) { - return response.json(); - } else { - throw new Error('Search index not found (HTTP ' + response.status + ') at ' + window.location.origin + indexUrl); + return basePath + '/index.json'; + }; + + // Lazy-load: returns a cached promise so multiple inputs share one fetch. + FuzzySearch.prototype.ensureLoaded = function () { + if (this.loadPromise) return this.loadPromise; + const self = this; + const indexUrl = this.getIndexUrl(); + + this.loadPromise = fetch(indexUrl) + .then((response) => { + if (!response.ok) { + throw new Error('Search index not found (HTTP ' + response.status + ') at ' + indexUrl); } + return response.json(); }) - .then(function(data) { + .then((data) => { if (Array.isArray(data) && data.length > 0) { self.searchData = data; - console.log('Search index loaded with ' + data.length + ' pages'); } else { - console.warn('Search index is empty or invalid'); self.searchData = []; } self.buildSearchIndex(); }) - .catch(function(error) { + .catch((error) => { console.warn('Could not load search data:', error.message); - console.info('GitHub Pages Debug: Ensure these Hugo config settings:'); - console.info('1. In hugo.toml: outputs.home = ["HTML", "RSS", "JSON"]'); - console.info('2. In params.toml: taxonomies.mainSections = ["posts", "docs", etc.]'); - console.info('3. File exists: layouts/index.json'); - console.info('4. Content exists in mainSections directories'); - console.info('5. For GitHub Pages: baseURL should match your site URL exactly'); - console.info('Current pathname:', currentPath); - console.info('Constructed base path:', basePath); - console.info('Final fetch URL:', window.location.origin + indexUrl); self.searchData = []; self.buildSearchIndex(); }); + + return this.loadPromise; }; - FuzzySearch.prototype.buildSearchIndex = function() { - var self = this; - this.searchIndex = this.searchData.map(function(item) { - return { - id: item.id, - title: item.title, - content: item.content, - summary: item.summary, - url: item.url, - date: item.date, - tags: item.tags, - categories: item.categories, - section: item.section, - type: item.type, - searchText: (item.title + ' ' + item.content + ' ' + (item.tags || []).join(' ') + ' ' + (item.categories || []).join(' ')).toLowerCase(), - titleWords: self.tokenize(item.title.toLowerCase()), - contentWords: self.tokenize((item.content || '').toLowerCase()) - }; - }); + FuzzySearch.prototype.buildSearchIndex = function () { + const self = this; + this.searchIndex = this.searchData.map((item) => ({ + id: item.id, + title: item.title, + content: item.content, + summary: item.summary, + url: item.url, + date: item.date, + tags: item.tags, + categories: item.categories, + section: item.section, + type: item.type, + searchText: (item.title + ' ' + item.content + ' ' + (item.tags || []).join(' ') + ' ' + (item.categories || []).join(' ')).toLowerCase(), + titleWords: self.tokenize(item.title.toLowerCase()), + contentWords: self.tokenize((item.content || '').toLowerCase()) + })); }; - FuzzySearch.prototype.tokenize = function(text) { - return text.split(/\s+/).filter(function(word) { - return word.length > 2; - }); + FuzzySearch.prototype.tokenize = function (text) { + return text.split(/\s+/).filter((word) => word.length > 2); }; - FuzzySearch.prototype.fuzzyMatch = function(pattern, text) { - var patternLength = pattern.length; - var textLength = text.length; - + FuzzySearch.prototype.fuzzyMatch = function (pattern, text) { + const patternLength = pattern.length; + const textLength = text.length; + if (patternLength === 0) return { score: 1, matches: [] }; if (textLength === 0) return { score: 0, matches: [] }; - - var matches = []; - var patternIndex = 0; - var score = 0; - var consecutiveMatches = 0; - var maxConsecutive = 0; - - for (var textIndex = 0; textIndex < textLength && patternIndex < patternLength; textIndex++) { + + const matches = []; + let patternIndex = 0; + let score = 0; + let consecutiveMatches = 0; + let maxConsecutive = 0; + + for (let textIndex = 0; textIndex < textLength && patternIndex < patternLength; textIndex++) { if (pattern[patternIndex] === text[textIndex]) { matches.push(textIndex); patternIndex++; @@ -121,40 +100,40 @@ consecutiveMatches = 0; } } - + if (patternIndex !== patternLength) { return { score: 0, matches: [] }; } - - var matchRatio = patternLength / textLength; - var consecutiveBonus = maxConsecutive / patternLength; - var finalScore = (score / (textLength * patternLength)) + matchRatio + consecutiveBonus; - + + const matchRatio = patternLength / textLength; + const consecutiveBonus = maxConsecutive / patternLength; + const finalScore = (score / (textLength * patternLength)) + matchRatio + consecutiveBonus; + return { score: finalScore, matches: matches }; }; - FuzzySearch.prototype.calculateWordScore = function(queryWords, itemWords) { - var self = this; - var score = 0; - for (var i = 0; i < queryWords.length; i++) { - for (var j = 0; j < itemWords.length; j++) { - var match = self.fuzzyMatch(queryWords[i], itemWords[j]); + FuzzySearch.prototype.calculateWordScore = function (queryWords, itemWords) { + const self = this; + let score = 0; + for (let i = 0; i < queryWords.length; i++) { + for (let j = 0; j < itemWords.length; j++) { + const match = self.fuzzyMatch(queryWords[i], itemWords[j]); score += match.score; } } return score; }; - FuzzySearch.prototype.highlightMatches = function(text, query) { - var escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - var regex = new RegExp('(' + escapedQuery + ')', 'gi'); + FuzzySearch.prototype.highlightMatches = function (text, query) { + const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp('(' + escapedQuery + ')', 'gi'); return text.replace(regex, '$1'); }; - FuzzySearch.prototype.search = function(query) { + FuzzySearch.prototype.search = function (query) { if (!query || query.length < 2) return []; - - if (this.searchIndex.length === 0) { + + if (!this.searchIndex || this.searchIndex.length === 0) { return [{ id: 'no-index', title: 'Search index not available', @@ -166,38 +145,38 @@ highlight: 'Search index not available' }]; } - - var normalizedQuery = query.toLowerCase().trim(); - var queryWords = this.tokenize(normalizedQuery); - var results = []; - var self = this; - - for (var i = 0; i < this.searchIndex.length; i++) { - var item = this.searchIndex[i]; + + const normalizedQuery = query.toLowerCase().trim(); + const queryWords = this.tokenize(normalizedQuery); + const results = []; + const self = this; + + for (let i = 0; i < this.searchIndex.length; i++) { + const item = this.searchIndex[i]; if (item.url === window.location.pathname) continue; - - var totalScore = 0; - - var directMatch = self.fuzzyMatch(normalizedQuery, item.searchText); + + let totalScore = 0; + + const directMatch = self.fuzzyMatch(normalizedQuery, item.searchText); if (directMatch.score > 0) { totalScore += directMatch.score * 2; } - - var titleScore = self.calculateWordScore(queryWords, item.titleWords) * 3; - var contentScore = self.calculateWordScore(queryWords, item.contentWords); - + + const titleScore = self.calculateWordScore(queryWords, item.titleWords) * 3; + const contentScore = self.calculateWordScore(queryWords, item.contentWords); + totalScore += titleScore + contentScore; - + if (item.title.toLowerCase().indexOf(normalizedQuery) !== -1) { totalScore += 5; } - - var escapedQuery = normalizedQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - var wordBoundaryRegex = new RegExp('\\b' + escapedQuery, 'i'); + + const escapedQuery = normalizedQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const wordBoundaryRegex = new RegExp('\\b' + escapedQuery, 'i'); if (wordBoundaryRegex.test(item.title) || wordBoundaryRegex.test(item.content)) { totalScore += 2; } - + if (totalScore > self.threshold) { results.push({ id: item.id, @@ -213,59 +192,62 @@ } } - results.sort(function(a, b) { - return b.score - a.score; - }); - + results.sort((a, b) => b.score - a.score); + return results.slice(0, this.maxResults); }; - FuzzySearch.prototype.setupSearchHandlers = function() { - var desktopInput = document.getElementById('search-input'); - var mobileInput = document.getElementById('mobile-search-input'); - + FuzzySearch.prototype.setupSearchHandlers = function () { + const desktopInput = document.getElementById('search-input'); + const mobileInput = document.getElementById('mobile-search-input'); + if (desktopInput) { this.setupSearchInput(desktopInput, 'desktop'); } - + if (mobileInput) { this.setupSearchInput(mobileInput, 'mobile'); } }; - FuzzySearch.prototype.setupSearchInput = function(input, type) { - var self = this; - var timeoutId; - var resultsContainer = this.createResultsContainer(input, type); + FuzzySearch.prototype.setupSearchInput = function (input, type) { + const self = this; + let timeoutId; + const resultsContainer = this.createResultsContainer(input, type); - input.addEventListener('input', function(e) { + // Lazy load: kick off fetch on first focus rather than on page load. + input.addEventListener('focus', () => { self.ensureLoaded(); }, { once: true }); + + input.addEventListener('input', (e) => { clearTimeout(timeoutId); - timeoutId = setTimeout(function() { - var query = e.target.value.trim(); + timeoutId = setTimeout(() => { + const query = e.target.value.trim(); if (query.length >= 2) { - var results = self.search(query); - self.displayResults(results, resultsContainer, query); + self.ensureLoaded().then(() => { + const results = self.search(query); + self.displayResults(results, resultsContainer, query); + }); } else { self.hideResults(resultsContainer); } }, 200); }); - input.addEventListener('keydown', function(e) { + input.addEventListener('keydown', (e) => { self.handleKeyboardNavigation(e, resultsContainer); }); - document.addEventListener('click', function(e) { + document.addEventListener('click', (e) => { if (!input.contains(e.target) && !resultsContainer.contains(e.target)) { self.hideResults(resultsContainer); } }); - var form = input.closest('form'); + const form = input.closest('form'); if (form) { - form.addEventListener('submit', function(e) { + form.addEventListener('submit', (e) => { e.preventDefault(); - var query = input.value.trim(); + const query = input.value.trim(); if (query) { self.performSearch(query); } @@ -273,12 +255,12 @@ } }; - FuzzySearch.prototype.createResultsContainer = function(input, type) { - var container = document.createElement('div'); + FuzzySearch.prototype.createResultsContainer = function (input, type) { + const container = document.createElement('div'); container.className = 'search-results search-results-' + type; container.style.cssText = 'position: absolute; top: 100%; left: 0; right: 0; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--border-radius); box-shadow: var(--shadow-lg); max-height: 400px; overflow-y: auto; z-index: var(--z-index-dropdown); display: none;'; - var wrapper = input.closest('.header-search, .mobile-search-form'); + const wrapper = input.closest('.header-search, .mobile-search-form'); if (wrapper) { wrapper.style.position = 'relative'; wrapper.appendChild(container); @@ -287,32 +269,31 @@ return container; }; - FuzzySearch.prototype.displayResults = function(results, container, query) { - var self = this; + FuzzySearch.prototype.displayResults = function (results, container, query) { if (results.length === 0) { container.innerHTML = '
No pages found for "' + query + '"
'; } else if (results.length === 1 && results[0].id === 'no-index') { - var result = results[0]; + const result = results[0]; container.innerHTML = '
' + result.title + '
' + result.summary + '
'; } else { - var resultsHTML = ''; - for (var i = 0; i < results.length; i++) { - var result = results[i]; - var bgColor = i === 0 ? 'background-color: var(--color-background);' : ''; - var displayText = result.summary || result.content || ''; - var truncatedText = this.truncateText(displayText, 100); - + let resultsHTML = ''; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const bgColor = i === 0 ? 'background-color: var(--color-background);' : ''; + const displayText = result.summary || result.content || ''; + const truncatedText = this.truncateText(displayText, 100); + resultsHTML += '
'; resultsHTML += '
' + result.highlight + '
'; resultsHTML += '
' + truncatedText + '
'; resultsHTML += '
PAGE
'; resultsHTML += '
'; } - + container.innerHTML = resultsHTML; - var resultItems = container.querySelectorAll('.search-result'); - for (var j = 0; j < resultItems.length; j++) { + const resultItems = container.querySelectorAll('.search-result'); + for (let j = 0; j < resultItems.length; j++) { this.attachResultHandlers(resultItems[j], container); } } @@ -320,29 +301,29 @@ container.style.display = 'block'; }; - FuzzySearch.prototype.attachResultHandlers = function(item, container) { - var self = this; + FuzzySearch.prototype.attachResultHandlers = function (item, container) { + const self = this; if (item.dataset.url !== '#') { - item.addEventListener('click', function() { + item.addEventListener('click', function () { window.location.href = this.dataset.url; }); - item.addEventListener('mouseenter', function() { + item.addEventListener('mouseenter', function () { self.setActiveResult(container, parseInt(this.dataset.index)); }); } }; - FuzzySearch.prototype.hideResults = function(container) { + FuzzySearch.prototype.hideResults = function (container) { container.style.display = 'none'; }; - FuzzySearch.prototype.handleKeyboardNavigation = function(e, container) { - var results = container.querySelectorAll('.search-result'); + FuzzySearch.prototype.handleKeyboardNavigation = function (e, container) { + const results = container.querySelectorAll('.search-result'); if (results.length === 0) return; - var activeIndex = this.getActiveResultIndex(container); - var newIndex = activeIndex; + const activeIndex = this.getActiveResultIndex(container); + let newIndex = activeIndex; switch (e.key) { case 'ArrowDown': @@ -369,13 +350,13 @@ } }; - FuzzySearch.prototype.getActiveResultIndex = function(container) { - var activeResult = container.querySelector('.search-result[style*="background-color"]'); + FuzzySearch.prototype.getActiveResultIndex = function (container) { + const activeResult = container.querySelector('.search-result[style*="background-color"]'); return activeResult ? parseInt(activeResult.dataset.index) : -1; }; - FuzzySearch.prototype.setActiveResult = function(container, index) { - container.querySelectorAll('.search-result').forEach(function(item, i) { + FuzzySearch.prototype.setActiveResult = function (container, index) { + container.querySelectorAll('.search-result').forEach((item, i) => { if (i === index) { item.style.backgroundColor = 'var(--color-background)'; } else { @@ -384,21 +365,20 @@ }); }; - FuzzySearch.prototype.truncateText = function(text, maxLength) { + FuzzySearch.prototype.truncateText = function (text, maxLength) { if (text.length <= maxLength) return text; return text.substring(0, maxLength).trim() + '...'; }; - FuzzySearch.prototype.performSearch = function(query) { - var searchUrl = '/search/?q=' + encodeURIComponent(query); + FuzzySearch.prototype.performSearch = function (query) { + const searchUrl = '/search/?q=' + encodeURIComponent(query); window.location.href = searchUrl; }; - document.addEventListener('DOMContentLoaded', function() { + document.addEventListener('DOMContentLoaded', () => { new FuzzySearch({ threshold: 0.2, maxResults: 8 }); }); - })(); diff --git a/assets/js/kokoro-tts.js b/assets/js/kokoro-tts.js deleted file mode 100644 index f2dcadb..0000000 --- a/assets/js/kokoro-tts.js +++ /dev/null @@ -1,599 +0,0 @@ -/** - * Kokoro TTS Backend — HD Voice module - * Wraps kokoro-js (Apache-2.0) for neural text-to-speech in the browser. - * - * Architecture: ONNX inference runs in a Web Worker so it never blocks - * the main thread — audio events, UI updates, and playback transitions - * stay responsive even while the model generates audio. - * - * Model: onnx-community/Kokoro-82M-v1.0-ONNX (~87 MB q8) - * The ONNX weights are downloaded once on user opt-in and cached - * automatically by the @huggingface/transformers Cache API layer. - * - * CSP requirements (if your site uses a Content-Security-Policy header): - * script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net - * connect-src 'self' https://huggingface.co https://*.huggingface.co https://*.hf.co - * worker-src 'self' blob: - * media-src 'self' blob: - */ -(() => { - 'use strict'; - - /* ── Configuration ─────────────────────────────────────────────── */ - const KOKORO_CDN = 'https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/dist/kokoro.web.js'; - const MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX'; - const DTYPE = 'q8'; - - /* ── Available voices ──────────────────────────────────────────── */ - const VOICES = [ - { id: 'af_heart', name: 'Heart', accent: 'US', gender: 'F' }, - { id: 'af_bella', name: 'Bella', accent: 'US', gender: 'F' }, - { id: 'af_nicole', name: 'Nicole', accent: 'US', gender: 'F' }, - { id: 'af_sarah', name: 'Sarah', accent: 'US', gender: 'F' }, - { id: 'af_sky', name: 'Sky', accent: 'US', gender: 'F' }, - { id: 'am_adam', name: 'Adam', accent: 'US', gender: 'M' }, - { id: 'am_michael', name: 'Michael', accent: 'US', gender: 'M' }, - { id: 'bf_emma', name: 'Emma', accent: 'GB', gender: 'F' }, - { id: 'bf_isabella', name: 'Isabella', accent: 'GB', gender: 'F' }, - { id: 'bm_george', name: 'George', accent: 'GB', gender: 'M' }, - { id: 'bm_lewis', name: 'Lewis', accent: 'GB', gender: 'M' }, - ]; - - /* ── WebGPU probe ──────────────────────────────────────────────── */ - - /** - * Probe for a usable WebGPU adapter. - * Returns 'webgpu' if a real adapter is obtained, otherwise 'wasm'. - * Brave exposes navigator.gpu but requestAdapter() returns null. - */ - async function detectDevice() { - if (typeof navigator !== 'undefined' && navigator.gpu) { - try { - const adapter = await navigator.gpu.requestAdapter(); - if (adapter) { - console.log('[kokoro] WebGPU adapter obtained ✅ — will use GPU inference'); - return 'webgpu'; - } - console.log('[kokoro] navigator.gpu exists but requestAdapter() returned null — using WASM'); - } catch (e) { - console.warn('[kokoro] WebGPU probe failed:', e.message, '— using WASM'); - } - } else { - console.log('[kokoro] No navigator.gpu — using WASM'); - } - return 'wasm'; - } - - /* ── Worker source code ────────────────────────────────────────── */ - - /** - * Build the JavaScript source for the inference Web Worker. - * The worker loads kokoro-js from CDN, initialises the ONNX model, - * and handles generate / set-voice messages. - * - * Communication protocol (postMessage): - * Main → Worker: - * { type:'init', id, modelId, dtype, device } - * { type:'generate', id, text, voice } - * { type:'set-voice', voice } - * Worker → Main: - * { type:'progress', status, progress, … } (during init) - * { type:'init-done', id, device } - * { type:'generated', id, samples:Float32Array, samplingRate, genMs } - * { type:'error', id, error:string } - */ - function buildWorkerSource() { - // Use array join to avoid template-literal issues in Hugo's js.Build - return [ - "import { KokoroTTS } from '" + KOKORO_CDN + "';", - "", - "let tts = null;", - "let currentVoice = 'af_heart';", - "", - "self.onmessage = async (e) => {", - " const { type, id } = e.data;", - " try {", - " if (type === 'init') {", - " let device = e.data.device;", - " try {", - " tts = await KokoroTTS.from_pretrained(e.data.modelId, {", - " dtype: e.data.dtype,", - " device: device,", - " progress_callback: (p) => self.postMessage({ type: 'progress', ...p }),", - " });", - " } catch (devErr) {", - " if (device === 'webgpu') {", - " device = 'wasm';", - " tts = await KokoroTTS.from_pretrained(e.data.modelId, {", - " dtype: e.data.dtype,", - " device: 'wasm',", - " progress_callback: (p) => self.postMessage({ type: 'progress', ...p }),", - " });", - " } else { throw devErr; }", - " }", - " self.postMessage({ type: 'init-done', id, device });", - " } else if (type === 'generate') {", - " const t0 = performance.now();", - " const raw = await tts.generate(e.data.text, { voice: e.data.voice || currentVoice });", - " const genMs = performance.now() - t0;", - " const samples = new Float32Array(raw.audio);", - " self.postMessage(", - " { type: 'generated', id, samples, samplingRate: raw.sampling_rate, genMs },", - " [samples.buffer]", - " );", - " } else if (type === 'set-voice') {", - " currentVoice = e.data.voice;", - " }", - " } catch (err) {", - " self.postMessage({ type: 'error', id, error: err.message || String(err) });", - " }", - "};", - ].join('\n'); - } - - /* ── Main-thread module loader (fallback only) ─────────────────── */ - - /** - * Try dynamic import() first. If the browser blocks it, fall back to - * injecting a {{ end }} diff --git a/layouts/partials/head/js.html b/layouts/partials/head/js.html index 88f92fd..2f9565f 100644 --- a/layouts/partials/head/js.html +++ b/layouts/partials/head/js.html @@ -18,7 +18,8 @@ {{- end }} {{- else }} - {{- $opts := dict "minify" true }} + {{- /* Production: minify and strip console.* / debugger statements. */}} + {{- $opts := dict "minify" true "drop" (slice "console" "debugger") }} {{- with . | js.Build $opts | fingerprint }} {{- end }}