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:
|
||||
# Persist data (Easy setup: Just maps a folder)
|
||||
- ./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)
|
||||
# - ./videos:/app/youtube_downloads
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLASK_ENV=production
|
||||
- COOKIES_FILE=/app/cookies.txt
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:5000/" ]
|
||||
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
|
||||
*/
|
||||
|
||||
class WebLLMService {
|
||||
constructor() {
|
||||
this.engine = null;
|
||||
this.isLoading = false;
|
||||
this.loadProgress = 0;
|
||||
this.currentModel = null;
|
||||
// Guard against redeclaration on SPA navigation
|
||||
if (typeof WebLLMService === 'undefined') {
|
||||
|
||||
// Model configurations - Qwen2 chosen for Vietnamese support
|
||||
this.models = {
|
||||
'qwen2-0.5b': 'Qwen2-0.5B-Instruct-q4f16_1-MLC',
|
||||
'phi-3.5-mini': 'Phi-3.5-mini-instruct-q4f16_1-MLC',
|
||||
'smollm2': 'SmolLM2-360M-Instruct-q4f16_1-MLC'
|
||||
};
|
||||
class WebLLMService {
|
||||
constructor() {
|
||||
this.engine = null;
|
||||
this.isLoading = false;
|
||||
this.loadProgress = 0;
|
||||
this.currentModel = null;
|
||||
|
||||
// Default to lightweight Qwen2 for Vietnamese support
|
||||
this.selectedModel = 'qwen2-0.5b';
|
||||
|
||||
// Callbacks
|
||||
this.onProgressCallback = null;
|
||||
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);
|
||||
}
|
||||
// Model configurations - Qwen2 chosen for Vietnamese support
|
||||
this.models = {
|
||||
'qwen2-0.5b': 'Qwen2-0.5B-Instruct-q4f16_1-MLC',
|
||||
'phi-3.5-mini': 'Phi-3.5-mini-instruct-q4f16_1-MLC',
|
||||
'smollm2': 'SmolLM2-360M-Instruct-q4f16_1-MLC'
|
||||
};
|
||||
|
||||
// Create engine
|
||||
this.engine = await webllm.CreateMLCEngine(modelId, {
|
||||
initProgressCallback: initProgressCallback
|
||||
});
|
||||
// Default to lightweight Qwen2 for Vietnamese support
|
||||
this.selectedModel = 'qwen2-0.5b';
|
||||
|
||||
this.currentModel = modelKey || this.selectedModel;
|
||||
this.isLoading = false;
|
||||
this.loadProgress = 100;
|
||||
// Callbacks
|
||||
this.onProgressCallback = null;
|
||||
this.onReadyCallback = null;
|
||||
this.onErrorCallback = null;
|
||||
}
|
||||
|
||||
console.log('WebLLM ready!');
|
||||
if (this.onReadyCallback) {
|
||||
this.onReadyCallback();
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('WebLLM initialization failed:', error);
|
||||
this.isLoading = false;
|
||||
|
||||
if (this.onErrorCallback) {
|
||||
this.onErrorCallback(error.message);
|
||||
if (this.engine && this.currentModel === (modelKey || this.selectedModel)) {
|
||||
console.log('WebLLM already initialized with this model');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.onProgressCallback = onProgress;
|
||||
|
||||
/**
|
||||
* Check if engine is ready
|
||||
*/
|
||||
isReady() {
|
||||
return this.engine !== null && !this.isLoading;
|
||||
}
|
||||
try {
|
||||
// Dynamic import of WebLLM
|
||||
const webllm = await import('https://esm.run/@mlc-ai/web-llm');
|
||||
|
||||
/**
|
||||
* Summarize text using local AI
|
||||
* @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.');
|
||||
}
|
||||
const modelId = this.models[modelKey || this.selectedModel];
|
||||
console.log('Loading WebLLM model:', modelId);
|
||||
|
||||
// Truncate text to avoid token limits
|
||||
const maxChars = 4000;
|
||||
if (text.length > maxChars) {
|
||||
text = text.substring(0, maxChars) + '...';
|
||||
}
|
||||
// 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}`);
|
||||
|
||||
const langInstruction = language === 'vi'
|
||||
? 'Respond in Vietnamese (Tiếng Việt).'
|
||||
: 'Respond in English.';
|
||||
if (this.onProgressCallback) {
|
||||
this.onProgressCallback(this.loadProgress, status);
|
||||
}
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{
|
||||
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}`
|
||||
// Create engine
|
||||
this.engine = await webllm.CreateMLCEngine(modelId, {
|
||||
initProgressCallback: initProgressCallback
|
||||
});
|
||||
|
||||
this.currentModel = modelKey || this.selectedModel;
|
||||
this.isLoading = false;
|
||||
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',
|
||||
'vi': 'Vietnamese (Tiếng Việt)'
|
||||
};
|
||||
/**
|
||||
* Check if engine is ready
|
||||
*/
|
||||
isReady() {
|
||||
return this.engine !== null && !this.isLoading;
|
||||
}
|
||||
|
||||
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
|
||||
/**
|
||||
* Summarize text using local AI
|
||||
* @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.');
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await this.engine.chat.completions.create({
|
||||
messages: messages,
|
||||
temperature: 0.3,
|
||||
max_tokens: 500
|
||||
});
|
||||
// Truncate text to avoid token limits
|
||||
const maxChars = 4000;
|
||||
if (text.length > maxChars) {
|
||||
text = text.substring(0, maxChars) + '...';
|
||||
}
|
||||
|
||||
return response.choices[0].message.content.trim();
|
||||
const langInstruction = language === 'vi'
|
||||
? 'Respond in Vietnamese (Tiếng Việt).'
|
||||
: 'Respond in English.';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const messages = [
|
||||
{
|
||||
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}`
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract key points from text
|
||||
* @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.');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const maxChars = 3000;
|
||||
if (text.length > maxChars) {
|
||||
text = text.substring(0, maxChars) + '...';
|
||||
/**
|
||||
* 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',
|
||||
'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.'
|
||||
: 'Respond in English.';
|
||||
/**
|
||||
* Extract key points from text
|
||||
* @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 = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `You extract the main IDEAS and CONCEPTS from video content. ${langInstruction} Focus on:
|
||||
const maxChars = 3000;
|
||||
if (text.length > maxChars) {
|
||||
text = text.substring(0, maxChars) + '...';
|
||||
}
|
||||
|
||||
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
|
||||
- Key insights or takeaways
|
||||
- Important facts or claims
|
||||
- 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.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
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);
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
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.');
|
||||
}
|
||||
|
||||
return fullResponse;
|
||||
const messages = [
|
||||
{ role: 'user', content: prompt }
|
||||
];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Stream chat error:', error);
|
||||
throw error;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models
|
||||
*/
|
||||
getModels() {
|
||||
return Object.keys(this.models).map(key => ({
|
||||
id: key,
|
||||
name: this.models[key],
|
||||
selected: key === this.selectedModel
|
||||
}));
|
||||
// Global singleton instance
|
||||
window.webLLMService = new WebLLMService();
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = 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;
|
||||
}
|
||||
} // End guard block for WebLLMService
|
||||
|
|
|
|||
|
|
@ -1703,10 +1703,11 @@
|
|||
|
||||
<script>
|
||||
// Track current language state and WebLLM status
|
||||
let currentSummaryLang = 'en';
|
||||
let webllmInitialized = false;
|
||||
let currentTranscriptText = null;
|
||||
let currentSummaryData = { en: null, vi: null };
|
||||
// Use var and guards to prevent redeclaration errors on SPA navigation
|
||||
if (typeof currentSummaryLang === 'undefined') var currentSummaryLang = 'en';
|
||||
if (typeof webllmInitialized === 'undefined') var webllmInitialized = false;
|
||||
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)
|
||||
function updateWebLLMProgress(percent, status) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue