144 lines
5.1 KiB
JavaScript
Executable file
144 lines
5.1 KiB
JavaScript
Executable file
/**
|
|
* KV-Tube WebAI Service
|
|
* Local AI chatbot for transcript Q&A using WebLLM
|
|
*
|
|
* Runs entirely in-browser, no server required after model download
|
|
*/
|
|
|
|
// WebLLM CDN import (lazy loaded)
|
|
var WEBLLM_CDN = 'https://esm.run/@mlc-ai/web-llm';
|
|
|
|
// Model options - using verified WebLLM model IDs
|
|
var AI_MODELS = {
|
|
small: { id: 'Qwen2-0.5B-Instruct-q4f16_1-MLC', name: 'Qwen2 (0.5B)', size: '350MB' },
|
|
medium: { id: 'Qwen2-1.5B-Instruct-q4f16_1-MLC', name: 'Qwen2 (1.5B)', size: '1GB' },
|
|
};
|
|
|
|
// Default to small model
|
|
var DEFAULT_MODEL = AI_MODELS.small;
|
|
|
|
if (typeof TranscriptAI === 'undefined') {
|
|
window.TranscriptAI = class TranscriptAI {
|
|
constructor() {
|
|
this.engine = null;
|
|
this.isLoading = false;
|
|
this.isReady = false;
|
|
this.transcript = '';
|
|
this.onProgressCallback = null;
|
|
this.onReadyCallback = null;
|
|
}
|
|
|
|
setTranscript(text) {
|
|
this.transcript = text.slice(0, 8000); // Limit context size
|
|
}
|
|
|
|
setCallbacks({ onProgress, onReady }) {
|
|
this.onProgressCallback = onProgress;
|
|
this.onReadyCallback = onReady;
|
|
}
|
|
|
|
async init() {
|
|
if (this.isReady || this.isLoading) return;
|
|
|
|
this.isLoading = true;
|
|
|
|
try {
|
|
// Dynamic import WebLLM
|
|
const { CreateMLCEngine } = await import(WEBLLM_CDN);
|
|
|
|
// Initialize engine with progress callback
|
|
this.engine = await CreateMLCEngine(DEFAULT_MODEL.id, {
|
|
initProgressCallback: (report) => {
|
|
if (this.onProgressCallback) {
|
|
this.onProgressCallback(report);
|
|
}
|
|
console.log('AI Load Progress:', report.text);
|
|
}
|
|
});
|
|
|
|
this.isReady = true;
|
|
this.isLoading = false;
|
|
|
|
if (this.onReadyCallback) {
|
|
this.onReadyCallback();
|
|
}
|
|
|
|
console.log('TranscriptAI ready with model:', DEFAULT_MODEL.name);
|
|
|
|
} catch (err) {
|
|
this.isLoading = false;
|
|
console.error('Failed to load AI model:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async ask(question) {
|
|
if (!this.isReady) {
|
|
throw new Error('AI not initialized');
|
|
}
|
|
|
|
const systemPrompt = this.transcript
|
|
? `You are a helpful AI assistant analyzing a video transcript. Answer the user's question based ONLY on the transcript content below. Be concise and direct. If the answer is not in the transcript, say so.\n\nTRANSCRIPT:\n${this.transcript}`
|
|
: `You are a helpful AI assistant for KV-Tube, a lightweight YouTube client. You can help the user with general questions, explain features of the app, or chat casually. Be concise and helpful.`;
|
|
|
|
try {
|
|
const response = await this.engine.chat.completions.create({
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: question }
|
|
],
|
|
max_tokens: 256,
|
|
temperature: 0.7,
|
|
});
|
|
|
|
return response.choices[0].message.content;
|
|
|
|
} catch (err) {
|
|
console.error('AI response error:', err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async *askStreaming(question) {
|
|
if (!this.isReady) {
|
|
throw new Error('AI not initialized');
|
|
}
|
|
|
|
const systemPrompt = this.transcript
|
|
? `You are a helpful AI assistant analyzing a video transcript. Answer the user's question based ONLY on the transcript content below. Be concise and direct. If the answer is not in the transcript, say so.\n\nTRANSCRIPT:\n${this.transcript}`
|
|
: `You are a helpful AI assistant for KV-Tube, a lightweight YouTube client. You can help the user with general questions, explain features of the app, or chat casually. Be concise and helpful.`;
|
|
|
|
const chunks = await this.engine.chat.completions.create({
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: question }
|
|
],
|
|
max_tokens: 256,
|
|
temperature: 0.7,
|
|
stream: true,
|
|
});
|
|
|
|
for await (const chunk of chunks) {
|
|
const delta = chunk.choices[0]?.delta?.content;
|
|
if (delta) {
|
|
yield delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
getModelInfo() {
|
|
return DEFAULT_MODEL;
|
|
}
|
|
|
|
isModelReady() {
|
|
return this.isReady;
|
|
}
|
|
|
|
isModelLoading() {
|
|
return this.isLoading;
|
|
}
|
|
}
|
|
|
|
// Global instance
|
|
window.transcriptAI = new TranscriptAI();
|
|
}
|