Webscraper / app.pyv1
Moklas's picture
Rename app.py to app.pyv1
a5d0c20 verified
Raw
History Blame Contribute Delete
25.9 kB
from flask import Flask, request, render_template_string, Response, stream_with_context
import os
import time
import json
import subprocess
import atexit
import requests
from huggingface_hub import hf_hub_download
app = Flask(__name__)
# --- ১. আসল ১-বিট Bonsai চালানোর জন্য PrismML-এর llama.cpp ফর্ক বিল্ড করা ---
# stock llama-cpp-python-এ Q1_0-এর দ্রুত কার্নেল নেই, তাই আসল স্পিড পেতে
# PrismML-এর নিজস্ব ফর্ক সোর্স থেকে বিল্ড করতে হয়। HF Spaces-এ pip install
# দিয়ে এটা সম্ভব না, তাই অ্যাপ স্টার্টআপের সময় নিজে থেকে git clone + cmake
# build করা হচ্ছে (একবারই হবে, বিল্ড হয়ে গেলে পরের রিস্টার্টে স্কিপ হবে)।
LLAMA_CPP_DIR = os.path.join(os.getcwd(), "llama.cpp")
LLAMA_SERVER_BIN = os.path.join(LLAMA_CPP_DIR, "build", "bin", "llama-server")
LLAMA_SERVER_PORT = 8081
LLAMA_SERVER_URL = f"http://127.0.0.1:{LLAMA_SERVER_PORT}"
MODEL_REPO = "prism-ml/Bonsai-1.7B-gguf"
MODEL_FILE = "Bonsai-1.7B-Q1_0.gguf"
server_process = None
llm_ready = False
def build_llama_cpp_fork():
"""PrismML-এর llama.cpp ফর্ক ক্লোন ও বিল্ড করে (Q1_0 কার্নেলসহ)।"""
if os.path.exists(LLAMA_SERVER_BIN):
print("✅ llama-server আগে থেকেই বিল্ড করা আছে, বিল্ড স্কিপ করা হলো")
return
print("⏳ PrismML-এর llama.cpp ফর্ক ক্লোন করা হচ্ছে...")
subprocess.run(
["git", "clone", "--depth", "1",
"https://github.com/PrismML-Eng/llama.cpp", LLAMA_CPP_DIR],
check=True
)
print("⏳ CMake কনফিগার করা হচ্ছে...")
subprocess.run(["cmake", "-B", "build"], cwd=LLAMA_CPP_DIR, check=True)
cpu_count = os.cpu_count() or 2
print(f"⏳ বিল্ড হচ্ছে ({cpu_count} থ্রেড দিয়ে)... এতে কয়েক মিনিট লাগতে পারে")
subprocess.run(
["cmake", "--build", "build", "-j", str(cpu_count), "--target", "llama-server"],
cwd=LLAMA_CPP_DIR, check=True
)
print("✅ বিল্ড সম্পূর্ণ!")
def start_llama_server(model_path):
"""বিল্ড হওয়া llama-server বাইনারি ব্যাকগ্রাউন্ডে চালু করে।"""
global server_process
cpu_count = os.cpu_count() or 2
print("⏳ llama-server চালু করা হচ্ছে...")
server_process = subprocess.Popen([
LLAMA_SERVER_BIN,
"-m", model_path,
"--host", "127.0.0.1",
"--port", str(LLAMA_SERVER_PORT),
"-t", str(cpu_count),
"-c", "2048"
])
atexit.register(server_process.terminate)
# সার্ভার রেডি হওয়া পর্যন্ত অপেক্ষা (হেলথ চেক)
for _ in range(90):
try:
r = requests.get(f"{LLAMA_SERVER_URL}/health", timeout=2)
if r.status_code == 200:
print("✅ llama-server প্রস্তুত!")
return True
except Exception:
pass
time.sleep(2)
print("⚠️ নির্ধারিত সময়ে llama-server রেডি হয়নি")
return False
try:
print("⏳ মডেল ডাউনলোড হচ্ছে...")
model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILE,
local_dir="./models",
token=None
)
print(f"✅ মডেল ডাউনলোড সম্পূর্ণ: {model_path}")
build_llama_cpp_fork()
llm_ready = start_llama_server(model_path)
except Exception as e:
print(f"⚠️ সেটআপে সমস্যা হয়েছে: {e}")
llm_ready = False
# --- ২. HTML টেমপ্লেট ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<title>Bonsai 8B চ্যাট (Hugging Face Space)</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
max-width: 900px;
margin: 20px auto;
padding: 15px;
background: #f0f2f5;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
margin: -20px -20px 20px -20px;
border-radius: 16px 16px 0 0;
}
.header h2 { margin: 0; font-weight: 400; }
.header small { opacity: 0.8; font-size: 14px; }
.chat-box {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
height: 400px;
overflow-y: auto;
margin-bottom: 15px;
border: 1px solid #e0e0e0;
}
.message {
margin: 8px 0;
padding: 10px 15px;
border-radius: 18px;
max-width: 80%;
word-wrap: break-word;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-msg {
background: #007bff;
color: white;
margin-left: auto;
text-align: right;
border-bottom-right-radius: 4px;
}
.bot-msg {
background: #e9ecef;
color: #333;
margin-right: auto;
border-bottom-left-radius: 4px;
white-space: pre-wrap;
}
.bot-msg.thinking {
background: #e9ecef;
color: #666;
font-style: italic;
}
.time-info {
font-size: 11px;
color: #a0aec0;
margin: -6px 4px 10px 4px;
text-align: left;
}
.input-area {
display: flex;
gap: 10px;
}
#userInput {
flex: 1;
padding: 12px 18px;
border: 2px solid #ddd;
border-radius: 25px;
font-size: 15px;
outline: none;
transition: 0.2s;
}
#userInput:focus {
border-color: #667eea;
}
button {
padding: 12px 28px;
background: #667eea;
color: white;
border: none;
border-radius: 25px;
font-size: 15px;
cursor: pointer;
transition: 0.2s;
white-space: nowrap;
}
button:hover {
background: #5a67d8;
transform: scale(1.02);
}
button:disabled {
background: #a0aec0;
cursor: not-allowed;
transform: none;
}
.status {
color: #718096;
font-size: 13px;
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.loader {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #e2e8f0;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.clear-btn {
background: #e53e3e;
padding: 8px 16px;
font-size: 13px;
}
.clear-btn:hover {
background: #c53030;
}
.footer {
margin-top: 15px;
text-align: center;
color: #a0aec0;
font-size: 12px;
}
.settings-btn {
background: #718096;
padding: 12px 18px;
}
.settings-btn:hover {
background: #5a6373;
}
.settings-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.settings-panel {
background: white;
border-radius: 16px;
padding: 25px;
width: 100%;
max-width: 420px;
max-height: 85vh;
overflow-y: auto;
}
.settings-panel h3 {
margin-top: 0;
margin-bottom: 20px;
}
.settings-panel label {
display: block;
font-weight: 600;
margin: 16px 0 8px;
color: #2d3748;
}
.settings-panel .row-label {
display: flex;
justify-content: space-between;
align-items: center;
}
.settings-panel .row-label span.value {
color: #2f855a;
font-weight: 700;
}
.settings-panel input[type="number"] {
width: 100%;
padding: 10px 14px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 15px;
box-sizing: border-box;
}
.settings-panel input[type="range"] {
width: 100%;
}
.settings-panel select {
width: 100%;
padding: 10px 14px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 15px;
background: #f7f7f7;
}
.settings-buttons {
display: flex;
gap: 10px;
margin-top: 25px;
}
.settings-buttons button {
flex: 1;
padding: 12px;
font-size: 14px;
}
.btn-cancel {
background: #e2e8f0 !important;
color: #2d3748 !important;
}
.btn-cancel:hover {
background: #cbd5e0 !important;
}
.btn-save {
background: #2f855a !important;
}
.btn-save:hover {
background: #276749 !important;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>🌳 Bonsai 8B (১-বিট) চ্যাট</h2>
<small>Hugging Face Space • CPU Inference • মডেল সাইজ: ~১.১৫ GB (Q1_0, 1-bit)</small>
</div>
<div class="chat-box" id="chatBox">
<div class="message bot-msg">👋 হ্যালো! আমি Bonsai 8B। আপনি কী জানতে চান?</div>
</div>
<div class="input-area">
<input type="text" id="userInput" placeholder="এখানে প্রশ্ন লিখুন..." />
<button id="sendBtn">পাঠান</button>
<button id="settingsBtn" class="settings-btn">⚙️</button>
<button id="clearBtn" class="clear-btn">🗑️</button>
</div>
<div class="status" id="status">
<span>✅</span> প্রস্তুত
</div>
<div class="footer">
Powered by PrismML llama.cpp fork (llama-server) • Bonsai 1.7B (Q1_0, 1-bit)
</div>
</div>
<!-- সেটিংস প্যানেল -->
<div class="settings-overlay" id="settingsOverlay">
<div class="settings-panel">
<h3>⚙️ প্যারামিটার সেটিংস</h3>
<label class="row-label">🌡️ Temperature <span class="value" id="tempValue">0.1</span></label>
<input type="range" id="tempSlider" min="0" max="1.5" step="0.1" value="0.1">
<label>📝 Max Tokens</label>
<input type="number" id="maxTokensInput" value="64">
<label>🎯 Top P</label>
<input type="number" step="0.1" min="0" max="1" id="topPInput" value="0.7">
<label>⬆️ Top K</label>
<input type="number" id="topKInput" value="20">
<label>🔁 Repetition Penalty</label>
<input type="number" step="0.1" id="repPenaltyInput" value="1.4">
<label>🎲 Do Sample</label>
<select id="doSampleSelect">
<option value="false" selected>False</option>
<option value="true">True</option>
</select>
<label>🧠 থিংক মোড (Think Mode)</label>
<select id="thinkModeSelect">
<option value="false" selected>বন্ধ (দ্রুত রেসপন্স)</option>
<option value="true">চালু (ধীর, কিন্তু বেশি চিন্তা করে উত্তর দেয়)</option>
</select>
<label>⚡ Use Cache</label>
<select id="useCacheSelect">
<option value="true" selected>True</option>
<option value="false">False</option>
</select>
<label>🌊 Stream</label>
<select id="streamSelect">
<option value="true" selected>True (স্ট্রিমিং)</option>
<option value="false">False</option>
</select>
<div class="settings-buttons">
<button id="settingsCancelBtn" class="btn-cancel">বাতিল</button>
<button id="settingsSaveBtn" class="btn-save">সংরক্ষণ করুন</button>
</div>
</div>
</div>
<script>
const chatBox = document.getElementById('chatBox');
const input = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const clearBtn = document.getElementById('clearBtn');
const status = document.getElementById('status');
// --- সেটিংস প্যানেল ---
const settingsBtn = document.getElementById('settingsBtn');
const settingsOverlay = document.getElementById('settingsOverlay');
const settingsCancelBtn = document.getElementById('settingsCancelBtn');
const settingsSaveBtn = document.getElementById('settingsSaveBtn');
const tempSlider = document.getElementById('tempSlider');
const tempValue = document.getElementById('tempValue');
let settings = {
temperature: 0.1,
max_tokens: 64,
top_p: 0.7,
top_k: 20,
repeat_penalty: 1.4,
do_sample: false,
think_mode: false,
use_cache: true,
stream: true
};
function openSettings() {
tempSlider.value = settings.temperature;
tempValue.textContent = settings.temperature;
document.getElementById('maxTokensInput').value = settings.max_tokens;
document.getElementById('topPInput').value = settings.top_p;
document.getElementById('topKInput').value = settings.top_k;
document.getElementById('repPenaltyInput').value = settings.repeat_penalty;
document.getElementById('doSampleSelect').value = String(settings.do_sample);
document.getElementById('thinkModeSelect').value = String(settings.think_mode);
document.getElementById('useCacheSelect').value = String(settings.use_cache);
document.getElementById('streamSelect').value = String(settings.stream);
settingsOverlay.style.display = 'flex';
}
function closeSettings() {
settingsOverlay.style.display = 'none';
}
function saveSettings() {
settings.temperature = parseFloat(tempSlider.value);
settings.max_tokens = parseInt(document.getElementById('maxTokensInput').value, 10);
settings.top_p = parseFloat(document.getElementById('topPInput').value);
settings.top_k = parseInt(document.getElementById('topKInput').value, 10);
settings.repeat_penalty = parseFloat(document.getElementById('repPenaltyInput').value);
settings.do_sample = document.getElementById('doSampleSelect').value === 'true';
settings.think_mode = document.getElementById('thinkModeSelect').value === 'true';
settings.use_cache = document.getElementById('useCacheSelect').value === 'true';
settings.stream = document.getElementById('streamSelect').value === 'true';
closeSettings();
}
settingsBtn.addEventListener('click', openSettings);
settingsCancelBtn.addEventListener('click', closeSettings);
settingsSaveBtn.addEventListener('click', saveSettings);
tempSlider.addEventListener('input', () => { tempValue.textContent = tempSlider.value; });
function addMessage(text, isUser = false, isThinking = false) {
const div = document.createElement('div');
div.className = `message ${isUser ? 'user-msg' : 'bot-msg'}${isThinking ? ' thinking' : ''}`;
div.textContent = text;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
return div;
}
async function sendMessage() {
const text = input.value.trim();
if (!text) return;
addMessage(text, true);
input.value = '';
sendBtn.disabled = true;
status.innerHTML = '<span class="loader"></span> চিন্তা করছি...';
// Thinking indicator
const thinkingDiv = addMessage('⏳ চিন্তা করছি...', false, true);
// সেন্ড বাটনে ক্লিক করার মুহূর্ত থেকেই সময় গোনা শুরু
const clickTime = performance.now();
let firstTokenTime = null;
try {
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: text, ...settings })
});
if (!response.ok) {
throw new Error('সার্ভার এরর');
}
thinkingDiv.remove();
const botDiv = addMessage('', false);
if (settings.stream && response.body) {
// --- স্ট্রিমিং মোড: টোকেন আসার সাথে সাথে দেখানো হয় ---
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunkText = decoder.decode(value, { stream: true });
if (chunkText) {
if (firstTokenTime === null) {
firstTokenTime = performance.now(); // প্রথম টোকেন আসার মুহূর্ত
}
fullText += chunkText;
botDiv.textContent = fullText;
chatBox.scrollTop = chatBox.scrollHeight;
}
}
} else {
// --- নন-স্ট্রিমিং মোড: পুরো রেসপন্স একসাথে আসে ---
const data = await response.json();
firstTokenTime = performance.now();
botDiv.textContent = data.error ? `⚠️ ${data.error}` : data.response;
}
const endTime = performance.now();
const timeToFirst = ((firstTokenTime ?? endTime) - clickTime) / 1000;
const totalTime = (endTime - clickTime) / 1000;
// প্রতিটা রেসপন্সের নিচে ছোট করে টাইমিং দেখানো হচ্ছে
const timeDiv = document.createElement('div');
timeDiv.className = 'time-info';
timeDiv.textContent =
`⏱️ রেসপন্স শুরু হতে: ${timeToFirst.toFixed(2)}s • সম্পূর্ণ হতে: ${totalTime.toFixed(2)}s`;
chatBox.appendChild(timeDiv);
chatBox.scrollTop = chatBox.scrollHeight;
status.innerHTML = '✅ প্রস্তুত';
} catch (error) {
thinkingDiv.remove();
addMessage('⚠️ সার্ভার ত্রুটি! আবার চেষ্টা করুন।', false);
status.innerHTML = '❌ সংযোগ ত্রুটি';
}
sendBtn.disabled = false;
input.focus();
}
function clearChat() {
chatBox.innerHTML = '';
addMessage('🗑️ চ্যাট ক্লিয়ার করা হয়েছে।', false);
addMessage('👋 নতুন করে শুরু করুন!', false);
status.innerHTML = '✅ প্রস্তুত';
}
sendBtn.addEventListener('click', sendMessage);
clearBtn.addEventListener('click', clearChat);
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Auto-focus on page load
input.focus();
</script>
</body>
</html>
"""
# --- ৩. Flask রাউট ---
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/chat', methods=['POST'])
def chat():
data = request.get_json()
user_prompt = data.get('prompt', '')
if not user_prompt:
return {"error": "কোনো প্রশ্ন নেই"}
if not llm_ready:
return {"error": "মডেল/সার্ভার প্রস্তুত না। সার্ভার লগ চেক করুন।"}
# --- ফ্রন্টএন্ড সেটিংস প্যানেল থেকে পাঠানো প্যারামিটার ---
max_tokens = int(data.get('max_tokens', 64))
temperature = float(data.get('temperature', 0.1))
top_p = float(data.get('top_p', 0.7))
top_k = int(data.get('top_k', 20))
repeat_penalty = float(data.get('repeat_penalty', 1.4))
do_sample = bool(data.get('do_sample', False))
think_mode = bool(data.get('think_mode', False))
use_stream = bool(data.get('stream', True))
use_cache = bool(data.get('use_cache', True)) # এখন সত্যিই llama-server-এর prompt cache নিয়ন্ত্রণ করে
# থিংক মোড বন্ধ থাকলে "/no_think" যোগ করা হচ্ছে
think_suffix = "" if think_mode else " /no_think"
full_prompt = f"<|im_start|>user\n{user_prompt}{think_suffix}<|im_end|>\n<|im_start|>assistant\n"
payload = {
"prompt": full_prompt,
"n_predict": max_tokens,
"temperature": temperature if do_sample else 0.0,
"top_p": top_p,
"top_k": top_k,
"repeat_penalty": repeat_penalty,
"cache_prompt": use_cache,
"stream": use_stream,
"stop": ["<|im_end|>", "user:", "User:"]
}
if use_stream:
def generate():
try:
with requests.post(f"{LLAMA_SERVER_URL}/completion", json=payload, stream=True, timeout=300) as r:
for line in r.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if line.startswith("data: "):
try:
chunk = json.loads(line[len("data: "):])
except json.JSONDecodeError:
continue
content = chunk.get("content", "")
if content:
yield content
if chunk.get("stop"):
break
except Exception as e:
print(f"❌ জেনারেশন ত্রুটি: {e}")
yield f"\n⚠️ মডেল ত্রুটি: {str(e)}"
return Response(stream_with_context(generate()), mimetype='text/plain')
else:
try:
r = requests.post(f"{LLAMA_SERVER_URL}/completion", json=payload, timeout=300)
result = r.json()
output = result.get("content", "").strip()
return {"response": output}
except Exception as e:
print(f"❌ জেনারেশন ত্রুটি: {e}")
return {"error": f"মডেল ত্রুটি: {str(e)}"}
# --- ৪. সার্ভার চালানো ---
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860)