v3.1.3: Fix SPA redeclaration errors, update docker-compose for cookies
Some checks failed
Docker Build & Push / build (push) Has been cancelled
Some checks failed
Docker Build & Push / build (push) Has been cancelled
This commit is contained in:
parent
663ef6ba44
commit
79f69772a0
3 changed files with 304 additions and 295 deletions
|
|
@ -14,11 +14,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
# Persist data (Easy setup: Just maps a folder)
|
# Persist data (Easy setup: Just maps a folder)
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
# Cookies file for YouTube (Required for NAS/Server to fix "Transcript not available")
|
||||||
|
- ./data/cookies.txt:/app/cookies.txt
|
||||||
# Local videos folder (Optional)
|
# Local videos folder (Optional)
|
||||||
# - ./videos:/app/youtube_downloads
|
# - ./videos:/app/youtube_downloads
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- FLASK_ENV=production
|
- FLASK_ENV=production
|
||||||
|
- COOKIES_FILE=/app/cookies.txt
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "curl", "-f", "http://localhost:5000/" ]
|
test: [ "CMD", "curl", "-f", "http://localhost:5000/" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
|
||||||
587
static/js/webllm-service.js
Normal file → Executable file
587
static/js/webllm-service.js
Normal file → Executable file
|
|
@ -3,338 +3,343 @@
|
||||||
* Uses MLC's WebLLM for on-device AI inference via WebGPU
|
* Uses MLC's WebLLM for on-device AI inference via WebGPU
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class WebLLMService {
|
// Guard against redeclaration on SPA navigation
|
||||||
constructor() {
|
if (typeof WebLLMService === 'undefined') {
|
||||||
this.engine = null;
|
|
||||||
this.isLoading = false;
|
|
||||||
this.loadProgress = 0;
|
|
||||||
this.currentModel = null;
|
|
||||||
|
|
||||||
// Model configurations - Qwen2 chosen for Vietnamese support
|
class WebLLMService {
|
||||||
this.models = {
|
constructor() {
|
||||||
'qwen2-0.5b': 'Qwen2-0.5B-Instruct-q4f16_1-MLC',
|
this.engine = null;
|
||||||
'phi-3.5-mini': 'Phi-3.5-mini-instruct-q4f16_1-MLC',
|
this.isLoading = false;
|
||||||
'smollm2': 'SmolLM2-360M-Instruct-q4f16_1-MLC'
|
this.loadProgress = 0;
|
||||||
};
|
this.currentModel = null;
|
||||||
|
|
||||||
// Default to lightweight Qwen2 for Vietnamese support
|
// Model configurations - Qwen2 chosen for Vietnamese support
|
||||||
this.selectedModel = 'qwen2-0.5b';
|
this.models = {
|
||||||
|
'qwen2-0.5b': 'Qwen2-0.5B-Instruct-q4f16_1-MLC',
|
||||||
// Callbacks
|
'phi-3.5-mini': 'Phi-3.5-mini-instruct-q4f16_1-MLC',
|
||||||
this.onProgressCallback = null;
|
'smollm2': 'SmolLM2-360M-Instruct-q4f16_1-MLC'
|
||||||
this.onReadyCallback = null;
|
|
||||||
this.onErrorCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if WebGPU is supported
|
|
||||||
*/
|
|
||||||
static isSupported() {
|
|
||||||
return 'gpu' in navigator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize WebLLM with selected model
|
|
||||||
* @param {string} modelKey - Model key from this.models
|
|
||||||
* @param {function} onProgress - Progress callback (percent, status)
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async init(modelKey = null, onProgress = null) {
|
|
||||||
if (!WebLLMService.isSupported()) {
|
|
||||||
console.warn('WebGPU not supported in this browser');
|
|
||||||
if (this.onErrorCallback) {
|
|
||||||
this.onErrorCallback('WebGPU not supported. Using server-side AI.');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.engine && this.currentModel === (modelKey || this.selectedModel)) {
|
|
||||||
console.log('WebLLM already initialized with this model');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
this.onProgressCallback = onProgress;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Dynamic import of WebLLM
|
|
||||||
const webllm = await import('https://esm.run/@mlc-ai/web-llm');
|
|
||||||
|
|
||||||
const modelId = this.models[modelKey || this.selectedModel];
|
|
||||||
console.log('Loading WebLLM model:', modelId);
|
|
||||||
|
|
||||||
// Progress callback wrapper
|
|
||||||
const initProgressCallback = (progress) => {
|
|
||||||
this.loadProgress = Math.round(progress.progress * 100);
|
|
||||||
const status = progress.text || 'Loading model...';
|
|
||||||
console.log(`WebLLM: ${this.loadProgress}% - ${status}`);
|
|
||||||
|
|
||||||
if (this.onProgressCallback) {
|
|
||||||
this.onProgressCallback(this.loadProgress, status);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create engine
|
// Default to lightweight Qwen2 for Vietnamese support
|
||||||
this.engine = await webllm.CreateMLCEngine(modelId, {
|
this.selectedModel = 'qwen2-0.5b';
|
||||||
initProgressCallback: initProgressCallback
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentModel = modelKey || this.selectedModel;
|
// Callbacks
|
||||||
this.isLoading = false;
|
this.onProgressCallback = null;
|
||||||
this.loadProgress = 100;
|
this.onReadyCallback = null;
|
||||||
|
this.onErrorCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('WebLLM ready!');
|
/**
|
||||||
if (this.onReadyCallback) {
|
* Check if WebGPU is supported
|
||||||
this.onReadyCallback();
|
*/
|
||||||
|
static isSupported() {
|
||||||
|
return 'gpu' in navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize WebLLM with selected model
|
||||||
|
* @param {string} modelKey - Model key from this.models
|
||||||
|
* @param {function} onProgress - Progress callback (percent, status)
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async init(modelKey = null, onProgress = null) {
|
||||||
|
if (!WebLLMService.isSupported()) {
|
||||||
|
console.warn('WebGPU not supported in this browser');
|
||||||
|
if (this.onErrorCallback) {
|
||||||
|
this.onErrorCallback('WebGPU not supported. Using server-side AI.');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
if (this.engine && this.currentModel === (modelKey || this.selectedModel)) {
|
||||||
|
console.log('WebLLM already initialized with this model');
|
||||||
} catch (error) {
|
return true;
|
||||||
console.error('WebLLM initialization failed:', error);
|
|
||||||
this.isLoading = false;
|
|
||||||
|
|
||||||
if (this.onErrorCallback) {
|
|
||||||
this.onErrorCallback(error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
this.isLoading = true;
|
||||||
}
|
this.onProgressCallback = onProgress;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Check if engine is ready
|
// Dynamic import of WebLLM
|
||||||
*/
|
const webllm = await import('https://esm.run/@mlc-ai/web-llm');
|
||||||
isReady() {
|
|
||||||
return this.engine !== null && !this.isLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const modelId = this.models[modelKey || this.selectedModel];
|
||||||
* Summarize text using local AI
|
console.log('Loading WebLLM model:', modelId);
|
||||||
* @param {string} text - Text to summarize
|
|
||||||
* @param {string} language - Output language ('en' or 'vi')
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async summarize(text, language = 'en') {
|
|
||||||
if (!this.isReady()) {
|
|
||||||
throw new Error('WebLLM not ready. Call init() first.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate text to avoid token limits
|
// Progress callback wrapper
|
||||||
const maxChars = 4000;
|
const initProgressCallback = (progress) => {
|
||||||
if (text.length > maxChars) {
|
this.loadProgress = Math.round(progress.progress * 100);
|
||||||
text = text.substring(0, maxChars) + '...';
|
const status = progress.text || 'Loading model...';
|
||||||
}
|
console.log(`WebLLM: ${this.loadProgress}% - ${status}`);
|
||||||
|
|
||||||
const langInstruction = language === 'vi'
|
if (this.onProgressCallback) {
|
||||||
? 'Respond in Vietnamese (Tiếng Việt).'
|
this.onProgressCallback(this.loadProgress, status);
|
||||||
: 'Respond in English.';
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const messages = [
|
// Create engine
|
||||||
{
|
this.engine = await webllm.CreateMLCEngine(modelId, {
|
||||||
role: 'system',
|
initProgressCallback: initProgressCallback
|
||||||
content: `You are a helpful AI assistant that creates detailed, insightful video summaries. ${langInstruction}`
|
});
|
||||||
},
|
|
||||||
{
|
this.currentModel = modelKey || this.selectedModel;
|
||||||
role: 'user',
|
this.isLoading = false;
|
||||||
content: `Provide a comprehensive summary of this video transcript in 4-6 sentences. Include the main topic, key points discussed, and any important insights or conclusions. Make the summary informative and meaningful:\n\n${text}`
|
this.loadProgress = 100;
|
||||||
|
|
||||||
|
console.log('WebLLM ready!');
|
||||||
|
if (this.onReadyCallback) {
|
||||||
|
this.onReadyCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebLLM initialization failed:', error);
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
if (this.onErrorCallback) {
|
||||||
|
this.onErrorCallback(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.engine.chat.completions.create({
|
|
||||||
messages: messages,
|
|
||||||
temperature: 0.7,
|
|
||||||
max_tokens: 350
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.choices[0].message.content.trim();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Summarization error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate text between English and Vietnamese
|
|
||||||
* @param {string} text - Text to translate
|
|
||||||
* @param {string} sourceLang - Source language ('en' or 'vi')
|
|
||||||
* @param {string} targetLang - Target language ('en' or 'vi')
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async translate(text, sourceLang = 'en', targetLang = 'vi') {
|
|
||||||
if (!this.isReady()) {
|
|
||||||
throw new Error('WebLLM not ready. Call init() first.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const langNames = {
|
/**
|
||||||
'en': 'English',
|
* Check if engine is ready
|
||||||
'vi': 'Vietnamese (Tiếng Việt)'
|
*/
|
||||||
};
|
isReady() {
|
||||||
|
return this.engine !== null && !this.isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = [
|
/**
|
||||||
{
|
* Summarize text using local AI
|
||||||
role: 'system',
|
* @param {string} text - Text to summarize
|
||||||
content: `You are a professional translator. Translate the following text from ${langNames[sourceLang]} to ${langNames[targetLang]}. Provide only the translation, no explanations.`
|
* @param {string} language - Output language ('en' or 'vi')
|
||||||
},
|
* @returns {Promise<string>}
|
||||||
{
|
*/
|
||||||
role: 'user',
|
async summarize(text, language = 'en') {
|
||||||
content: text
|
if (!this.isReady()) {
|
||||||
|
throw new Error('WebLLM not ready. Call init() first.');
|
||||||
}
|
}
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
// Truncate text to avoid token limits
|
||||||
const response = await this.engine.chat.completions.create({
|
const maxChars = 4000;
|
||||||
messages: messages,
|
if (text.length > maxChars) {
|
||||||
temperature: 0.3,
|
text = text.substring(0, maxChars) + '...';
|
||||||
max_tokens: 500
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return response.choices[0].message.content.trim();
|
const langInstruction = language === 'vi'
|
||||||
|
? 'Respond in Vietnamese (Tiếng Việt).'
|
||||||
|
: 'Respond in English.';
|
||||||
|
|
||||||
} catch (error) {
|
const messages = [
|
||||||
console.error('Translation error:', error);
|
{
|
||||||
throw error;
|
role: 'system',
|
||||||
}
|
content: `You are a helpful AI assistant that creates detailed, insightful video summaries. ${langInstruction}`
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `Provide a comprehensive summary of this video transcript in 4-6 sentences. Include the main topic, key points discussed, and any important insights or conclusions. Make the summary informative and meaningful:\n\n${text}`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Extract key points from text
|
const response = await this.engine.chat.completions.create({
|
||||||
* @param {string} text - Text to analyze
|
messages: messages,
|
||||||
* @param {string} language - Output language
|
temperature: 0.7,
|
||||||
* @returns {Promise<string[]>}
|
max_tokens: 350
|
||||||
*/
|
});
|
||||||
async extractKeyPoints(text, language = 'en') {
|
|
||||||
if (!this.isReady()) {
|
return response.choices[0].message.content.trim();
|
||||||
throw new Error('WebLLM not ready. Call init() first.');
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Summarization error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxChars = 3000;
|
/**
|
||||||
if (text.length > maxChars) {
|
* Translate text between English and Vietnamese
|
||||||
text = text.substring(0, maxChars) + '...';
|
* @param {string} text - Text to translate
|
||||||
|
* @param {string} sourceLang - Source language ('en' or 'vi')
|
||||||
|
* @param {string} targetLang - Target language ('en' or 'vi')
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async translate(text, sourceLang = 'en', targetLang = 'vi') {
|
||||||
|
if (!this.isReady()) {
|
||||||
|
throw new Error('WebLLM not ready. Call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const langNames = {
|
||||||
|
'en': 'English',
|
||||||
|
'vi': 'Vietnamese (Tiếng Việt)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `You are a professional translator. Translate the following text from ${langNames[sourceLang]} to ${langNames[targetLang]}. Provide only the translation, no explanations.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.engine.chat.completions.create({
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.3,
|
||||||
|
max_tokens: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.choices[0].message.content.trim();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Translation error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const langInstruction = language === 'vi'
|
/**
|
||||||
? 'Respond in Vietnamese.'
|
* Extract key points from text
|
||||||
: 'Respond in English.';
|
* @param {string} text - Text to analyze
|
||||||
|
* @param {string} language - Output language
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
async extractKeyPoints(text, language = 'en') {
|
||||||
|
if (!this.isReady()) {
|
||||||
|
throw new Error('WebLLM not ready. Call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
const messages = [
|
const maxChars = 3000;
|
||||||
{
|
if (text.length > maxChars) {
|
||||||
role: 'system',
|
text = text.substring(0, maxChars) + '...';
|
||||||
content: `You extract the main IDEAS and CONCEPTS from video content. ${langInstruction} Focus on:
|
}
|
||||||
|
|
||||||
|
const langInstruction = language === 'vi'
|
||||||
|
? 'Respond in Vietnamese.'
|
||||||
|
: 'Respond in English.';
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `You extract the main IDEAS and CONCEPTS from video content. ${langInstruction} Focus on:
|
||||||
- Main topics discussed
|
- Main topics discussed
|
||||||
- Key insights or takeaways
|
- Key insights or takeaways
|
||||||
- Important facts or claims
|
- Important facts or claims
|
||||||
- Conclusions or recommendations
|
- Conclusions or recommendations
|
||||||
|
|
||||||
Do NOT copy sentences from the transcript. Instead, synthesize the core ideas in your own words. List 3-5 key points, one per line, without bullet points or numbers.`
|
Do NOT copy sentences from the transcript. Instead, synthesize the core ideas in your own words. List 3-5 key points, one per line, without bullet points or numbers.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `What are the main ideas and takeaways from this video transcript?\n\n${text}`
|
content: `What are the main ideas and takeaways from this video transcript?\n\n${text}`
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.engine.chat.completions.create({
|
|
||||||
messages: messages,
|
|
||||||
temperature: 0.6,
|
|
||||||
max_tokens: 400
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = response.choices[0].message.content.trim();
|
|
||||||
const points = content.split('\n')
|
|
||||||
.map(line => line.replace(/^[\d\.\-\*\•]+\s*/, '').trim())
|
|
||||||
.filter(line => line.length > 10);
|
|
||||||
|
|
||||||
return points.slice(0, 5);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Key points extraction error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream chat completion for real-time output
|
|
||||||
* @param {string} prompt - User prompt
|
|
||||||
* @param {function} onChunk - Callback for each chunk
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async streamChat(prompt, onChunk) {
|
|
||||||
if (!this.isReady()) {
|
|
||||||
throw new Error('WebLLM not ready.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = [
|
|
||||||
{ role: 'user', content: prompt }
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const chunks = await this.engine.chat.completions.create({
|
|
||||||
messages: messages,
|
|
||||||
temperature: 0.7,
|
|
||||||
stream: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let fullResponse = '';
|
|
||||||
for await (const chunk of chunks) {
|
|
||||||
const delta = chunk.choices[0]?.delta?.content || '';
|
|
||||||
fullResponse += delta;
|
|
||||||
if (onChunk) {
|
|
||||||
onChunk(delta, fullResponse);
|
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.engine.chat.completions.create({
|
||||||
|
messages: messages,
|
||||||
|
temperature: 0.6,
|
||||||
|
max_tokens: 400
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = response.choices[0].message.content.trim();
|
||||||
|
const points = content.split('\n')
|
||||||
|
.map(line => line.replace(/^[\d\.\-\*\•]+\s*/, '').trim())
|
||||||
|
.filter(line => line.length > 10);
|
||||||
|
|
||||||
|
return points.slice(0, 5);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Key points extraction error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream chat completion for real-time output
|
||||||
|
* @param {string} prompt - User prompt
|
||||||
|
* @param {function} onChunk - Callback for each chunk
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async streamChat(prompt, onChunk) {
|
||||||
|
if (!this.isReady()) {
|
||||||
|
throw new Error('WebLLM not ready.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullResponse;
|
const messages = [
|
||||||
|
{ role: 'user', content: prompt }
|
||||||
|
];
|
||||||
|
|
||||||
} catch (error) {
|
try {
|
||||||
console.error('Stream chat error:', error);
|
const chunks = await this.engine.chat.completions.create({
|
||||||
throw error;
|
messages: messages,
|
||||||
|
temperature: 0.7,
|
||||||
|
stream: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let fullResponse = '';
|
||||||
|
for await (const chunk of chunks) {
|
||||||
|
const delta = chunk.choices[0]?.delta?.content || '';
|
||||||
|
fullResponse += delta;
|
||||||
|
if (onChunk) {
|
||||||
|
onChunk(delta, fullResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullResponse;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream chat error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models
|
||||||
|
*/
|
||||||
|
getModels() {
|
||||||
|
return Object.keys(this.models).map(key => ({
|
||||||
|
id: key,
|
||||||
|
name: this.models[key],
|
||||||
|
selected: key === this.selectedModel
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set selected model (requires re-init)
|
||||||
|
*/
|
||||||
|
setModel(modelKey) {
|
||||||
|
if (this.models[modelKey]) {
|
||||||
|
this.selectedModel = modelKey;
|
||||||
|
// Reset engine to force reload with new model
|
||||||
|
this.engine = null;
|
||||||
|
this.currentModel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup and release resources
|
||||||
|
*/
|
||||||
|
async destroy() {
|
||||||
|
if (this.engine) {
|
||||||
|
// WebLLM doesn't have explicit destroy, but we can nullify
|
||||||
|
this.engine = null;
|
||||||
|
this.currentModel = null;
|
||||||
|
this.loadProgress = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Global singleton instance
|
||||||
* Get available models
|
window.webLLMService = new WebLLMService();
|
||||||
*/
|
|
||||||
getModels() {
|
// Export for module usage
|
||||||
return Object.keys(this.models).map(key => ({
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
id: key,
|
module.exports = WebLLMService;
|
||||||
name: this.models[key],
|
|
||||||
selected: key === this.selectedModel
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
} // End guard block for WebLLMService
|
||||||
* Set selected model (requires re-init)
|
|
||||||
*/
|
|
||||||
setModel(modelKey) {
|
|
||||||
if (this.models[modelKey]) {
|
|
||||||
this.selectedModel = modelKey;
|
|
||||||
// Reset engine to force reload with new model
|
|
||||||
this.engine = null;
|
|
||||||
this.currentModel = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup and release resources
|
|
||||||
*/
|
|
||||||
async destroy() {
|
|
||||||
if (this.engine) {
|
|
||||||
// WebLLM doesn't have explicit destroy, but we can nullify
|
|
||||||
this.engine = null;
|
|
||||||
this.currentModel = null;
|
|
||||||
this.loadProgress = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global singleton instance
|
|
||||||
window.webLLMService = new WebLLMService();
|
|
||||||
|
|
||||||
// Export for module usage
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = WebLLMService;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1703,10 +1703,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Track current language state and WebLLM status
|
// Track current language state and WebLLM status
|
||||||
let currentSummaryLang = 'en';
|
// Use var and guards to prevent redeclaration errors on SPA navigation
|
||||||
let webllmInitialized = false;
|
if (typeof currentSummaryLang === 'undefined') var currentSummaryLang = 'en';
|
||||||
let currentTranscriptText = null;
|
if (typeof webllmInitialized === 'undefined') var webllmInitialized = false;
|
||||||
let currentSummaryData = { en: null, vi: null };
|
if (typeof currentTranscriptText === 'undefined') var currentTranscriptText = null;
|
||||||
|
if (typeof currentSummaryData === 'undefined') var currentSummaryData = { en: null, vi: null };
|
||||||
|
|
||||||
// WebLLM Progress Update Handler - Silent loading (just log to console)
|
// WebLLM Progress Update Handler - Silent loading (just log to console)
|
||||||
function updateWebLLMProgress(percent, status) {
|
function updateWebLLMProgress(percent, status) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue