from flask import Flask, request, render_template_string, Response, stream_with_context
from llama_cpp import Llama
import os
import time
import json
app = Flask(__name__)
# --- ১. সঠিক মডেল লোকেশন ---
# Q1_0 (এন্ড-টু-এন্ড ১-বিট) ফরম্যাট এখন mainline llama.cpp-তে upstream সাপোর্টেড,
# তাই llama-cpp-python (রিসেন্ট ভার্সন) দিয়ে এটা সরাসরি চলে।
# (শুধু তাদের ternary/Q2_0 ফরম্যাটের জন্য বিশেষ ফর্ক লাগে, Q1_0-এর জন্য না।)
# ✅ বর্তমানে সক্রিয়: Bonsai 8B (~1.16 GB)
#MODEL_REPO = "prism-ml/Bonsai-8B-gguf"
#MODEL_FILE = "Bonsai-8B-Q1_0.gguf"
# 🔽 ছোট মডেল চাইলে উপরের দুই লাইন কমেন্ট করে নিচের যেকোনো একটা আনকমেন্ট করুন 🔽
# --- Bonsai 4B (~0.57 GB) — মাঝারি সাইজ, আরও দ্রুত ---
#MODEL_REPO = "prism-ml/Bonsai-4B-gguf"
#MODEL_FILE = "Bonsai-4B-Q1_0.gguf"
# Google gemma3
MODEL_REPO = "DevQuasar/google.gemma-3n-E2B-it-GGUF"
MODEL_FILE = "google.gemma-3n-E2B-it.Q2_K.gguf"
# --- Bonsai 1.7B (~0.25 GB) — সবচেয়ে ছোট (১ বিলিয়নের কাছাকাছি), সবচেয়ে দ্রুত ---
#MODEL_REPO = "prism-ml/Bonsai-1.7B-gguf"
#MODEL_FILE = "Bonsai-1.7B-Q1_0.gguf"
print("⏳ মডেল ডাউনলোড হচ্ছে... (প্রথমবার একটু সময় লাগবে)")
from huggingface_hub import hf_hub_download
model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILE,
local_dir="./models",
token=None # পাবলিক রিপো, টোকেন লাগবে না
)
print("✅ ডাউনলোড সম্পূর্ণ!")
print(f"📁 ফাইল লোকেশন: {model_path}")
print("⏳ মডেল লোড হচ্ছে (CPU)...")
try:
# llama-cpp-python: GGUF মেটাডেটা থেকে নিজেই আর্কিটেকচার (qwen3) বুঝে নেয়,
# তাই model_type জাতীয় কিছু দিতে হয় না।
# CPU-এর সব কোর ব্যবহার করা হচ্ছে যাতে prompt processing (prefill) দ্রুত হয়।
# n_threads -> টোকেন জেনারেশনের সময় ব্যবহৃত থ্রেড সংখ্যা
# n_threads_batch -> prompt/prefill প্রসেসিংয়ের সময় ব্যবহৃত থ্রেড সংখ্যা (এটাই প্রথম টোকেনের দেরির মূল কারণ)
cpu_count = os.cpu_count() or 4
llm = Llama(
model_path=model_path,
n_ctx=2048,
n_threads=cpu_count,
n_threads_batch=cpu_count,
n_batch=512, # prefill ব্যাচ সাইজ, বড় করলে prompt processing দ্রুত হয়
verbose=False
)
# --- ওয়ার্মআপ ---
# প্রথম ইনফারেন্স কলে llama.cpp কিছু অভ্যন্তরীণ বাফার/গ্রাফ তৈরি করে যা এক্সট্রা সময় নেয়।
# সার্ভার চালু হওয়ার সময়ই একটা ডামি জেনারেশন চালিয়ে সেই ওয়ান-টাইম খরচ আগেই সেরে ফেলা হচ্ছে,
# যাতে ইউজারের প্রথম আসল রিকোয়েস্টে এই পেনাল্টি না লাগে।
print("🔥 মডেল ওয়ার্মআপ হচ্ছে...")
list(llm("<|im_start|>user\nহাই<|im_end|>\n<|im_start|>assistant\n\n\n\n\n",
max_tokens=1, stream=True))
print("✅ ওয়ার্মআপ সম্পন্ন!")
print("✅ মডেল প্রস্তুত! সার্ভার চালু হচ্ছে...")
except Exception as e:
print(f"⚠️ llama-cpp-python লোড করতে সমস্যা: {e}")
llm = None
# --- ২. HTML টেমপ্লেট ---
HTML_TEMPLATE = """
Bonsai 8B চ্যাট (Hugging Face Space)
🌳 Bonsai 8B (১-বিট) চ্যাট
Hugging Face Space • CPU Inference • মডেল সাইজ: ~১.১৫ GB (Q1_0, 1-bit)
➕ নতুন চ্যাট
👋 হ্যালো! আমি Bonsai 8B। আপনি কী জানতে চান?
✅ প্রস্তুত
"""
# --- ৩. 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', '')
# ফ্রন্টএন্ড থেকে পাঠানো সর্বশেষ (সর্বোচ্চ ৩টা) প্রশ্ন-উত্তরের হিস্টরি
# প্রতিটা আইটেম: {"user": "...", "assistant": "..."}
history = data.get('history', [])
if not user_prompt:
return {"error": "কোনো প্রশ্ন নেই"}
if llm is None:
return {"error": "মডেল লোড হয়নি। সার্ভার লগ চেক করুন।"}
# শুধু সর্বশেষ ৩টা এক্সচেঞ্জ ব্যবহার করা হচ্ছে (কনটেক্সট উইন্ডো সীমিত রাখতে)
if isinstance(history, list):
history = history[-3:]
else:
history = []
# থিংকিং সম্পূর্ণভাবে বন্ধ — খালি প্রি-ফিল করে মডেলকে সরাসরি উত্তরে পাঠানো হচ্ছে
# হিস্টরির প্রতিটা টার্ন প্রম্পটে জুড়ে দেওয়া হচ্ছে, যাতে মডেল আগের কনভারসেশন মনে রাখে
full_prompt = ""
for turn in history:
h_user = str(turn.get('user', '')).strip()
h_assistant = str(turn.get('assistant', '')).strip()
if not h_user:
continue
full_prompt += f"<|im_start|>user\n{h_user}<|im_end|>\n<|im_start|>assistant\n\n\n\n\n{h_assistant}<|im_end|>\n"
full_prompt += f"<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n\n\n\n\n"
def generate():
start_time = time.time()
start_str = time.strftime('%H:%M:%S', time.localtime(start_time))
first_token_time = None
try:
# stream=True দিলে llama-cpp-python টোকেন-বাই-টোকেন জেনারেট করে দেয়
stream = llm(
full_prompt,
max_tokens=256, # সর্বোচ্চ টোকেন সংখ্যা
temperature=0.1, # তাপমাত্রা (কম = ডিটারমিনিস্টিক)
top_p=0.2, # নিউক্লিয়াস স্যাম্পলিং
top_k=20, # টপ-কে স্যাম্পলিং
repeat_penalty=1.4, # পুনরাবৃত্তি শাস্তি
stop=["<|im_end|>", "user:", "User:"],
stream=True
)
for chunk in stream:
token_text = chunk["choices"][0]["text"]
if token_text:
# প্রথম টোকেনের সময় রেকর্ড
if first_token_time is None:
first_token_time = time.time() - start_time
yield token_text
except Exception as e:
print(f"❌ জেনারেশন ত্রুটি: {e}")
yield f"\n⚠️ মডেল ত্রুটি: {str(e)}"
finally:
end_time = time.time()
end_str = time.strftime('%H:%M:%S', time.localtime(end_time))
elapsed = round(end_time - start_time, 2)
first_token = round(first_token_time, 2) if first_token_time else 0
print(f"⏱️ প্রথম টোকেন: {first_token} সেকেন্ড | মোট সময়: {elapsed} সেকেন্ড")
print(f"📊 প্যারামিটার: max_tokens=256, temp=0.1, top_p=0.2, top_k=40, repeat_penalty=1.1")
# স্ট্রিমের একদম শেষে টাইমিং তথ্য পাঠানো
meta = {
"start": start_str,
"end": end_str,
"elapsed": elapsed,
"first_token_time": first_token
}
yield f"\n[[META]]{json.dumps(meta, ensure_ascii=False)}"
return Response(stream_with_context(generate()), mimetype='text/plain')
# --- ৪. সার্ভার চালানো ---
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860)