feat: complete JS rewrite - Transformers.js + Node.js + Mission Barisal Checker
Browse files- README.md +56 -7
- app.js +400 -0
- app.py +0 -375
- package.json +15 -0
- public/index.html +224 -0
- requirements.txt +0 -7
README.md
CHANGED
|
@@ -1,14 +1,63 @@
|
|
| 1 |
---
|
| 2 |
-
title: Sahon
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: green
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
python_version: '3.12'
|
| 9 |
-
app_file: app.
|
| 10 |
pinned: false
|
| 11 |
-
short_description:
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Sahon AI
|
| 3 |
+
emoji: 🤖
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: green
|
| 6 |
+
sdk: nodejs
|
| 7 |
+
sdk_version: 22
|
| 8 |
python_version: '3.12'
|
| 9 |
+
app_file: app.js
|
| 10 |
pinned: false
|
| 11 |
+
short_description: Transformers.js AI with Mission Barisal Checker
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Sahon AI 🤖
|
| 15 |
+
|
| 16 |
+
**Pure JavaScript LLM Inference** using `@huggingface/transformers` (Transformers.js).
|
| 17 |
+
|
| 18 |
+
- **Model:** `Xenova/phi-3-mini-4k-instruct` (April 2024 | 3.8B params)
|
| 19 |
+
- **Engine:** Transformers.js via ONNX Runtime
|
| 20 |
+
- **Checker:** Mission Barisal Response Validation
|
| 21 |
+
- **Zero Python!** Everything runs in Node.js
|
| 22 |
+
|
| 23 |
+
## API Endpoints (OpenAI-Compatible)
|
| 24 |
+
|
| 25 |
+
| Endpoint | Method | Description |
|
| 26 |
+
|---|---|---|
|
| 27 |
+
| `/v1/models` | GET | List available models |
|
| 28 |
+
| `/v1/chat/completions` | POST | Chat completions |
|
| 29 |
+
| `/v1/completions` | POST | Text completions |
|
| 30 |
+
| `/health` | GET | Health check |
|
| 31 |
+
|
| 32 |
+
### Usage
|
| 33 |
+
|
| 34 |
+
```python
|
| 35 |
+
from openai import OpenAI
|
| 36 |
+
|
| 37 |
+
client = OpenAI(
|
| 38 |
+
base_url="https://bdzombie-sahon.hf.space/v1",
|
| 39 |
+
api_key="not-needed"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
response = client.chat.completions.create(
|
| 43 |
+
model="phi-3-mini-4k-instruct",
|
| 44 |
+
messages=[{"role": "user", "content": "Hello!"}]
|
| 45 |
+
)
|
| 46 |
+
print(response.choices[0].message.content)
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Mission Barisal Checker
|
| 50 |
+
|
| 51 |
+
Every response is validated for:
|
| 52 |
+
- **Bias detection** — neutral language analysis
|
| 53 |
+
- **Hallucination check** — hedging/uncertainty detection
|
| 54 |
+
- **Quality scoring** — overall response quality metric
|
| 55 |
+
|
| 56 |
+
Results appear in API responses under `_mission_barisal` field.
|
| 57 |
+
|
| 58 |
+
## Tech Stack
|
| 59 |
+
|
| 60 |
+
- `@huggingface/transformers` — Transformers.js v3
|
| 61 |
+
- `express` — Web server
|
| 62 |
+
- ONNX Runtime (WASM) — Model inference
|
| 63 |
+
- Phi-3 Mini 4K Instruct — Microsoft, April 2024
|
app.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Sahon AI - Transformers.js Server
|
| 3 |
+
* ==================================
|
| 4 |
+
* Pure JavaScript LLM inference with OpenAI-compatible API
|
| 5 |
+
* and Mission Barisal Response Checker
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { pipeline } from '@huggingface/transformers';
|
| 9 |
+
import express from 'express';
|
| 10 |
+
import cors from 'cors';
|
| 11 |
+
import { fileURLToPath } from 'url';
|
| 12 |
+
import { dirname, join } from 'path';
|
| 13 |
+
import fs from 'fs';
|
| 14 |
+
|
| 15 |
+
// ─── Config ───
|
| 16 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 17 |
+
const __dirname = dirname(__filename);
|
| 18 |
+
const PORT = parseInt(process.env.PORT || '7860');
|
| 19 |
+
const HOST = '0.0.0.0';
|
| 20 |
+
|
| 21 |
+
// Use Phi-3-mini-4k-instruct (April 2024, pre-converted ONNX)
|
| 22 |
+
// Fits user's timeframe: mid-2024, real model (not agent)
|
| 23 |
+
const MODEL_ID = 'Xenova/phi-3-mini-4k-instruct';
|
| 24 |
+
|
| 25 |
+
// ─── State ───
|
| 26 |
+
let generator = null;
|
| 27 |
+
let modelReady = false;
|
| 28 |
+
let modelError = null;
|
| 29 |
+
let modelProgress = 'Initializing...';
|
| 30 |
+
|
| 31 |
+
// ─── Mission Barisal Checker Module ───
|
| 32 |
+
// Validates model outputs for bias, hallucination, and quality
|
| 33 |
+
const missionBarisalChecker = {
|
| 34 |
+
/**
|
| 35 |
+
* Check response for potential bias
|
| 36 |
+
* @param {string} question - Original question
|
| 37 |
+
* @param {string} response - Model's response
|
| 38 |
+
* @returns {{biased: boolean, reasons: string[]}}
|
| 39 |
+
*/
|
| 40 |
+
checkBias(question, response) {
|
| 41 |
+
const reasons = [];
|
| 42 |
+
const lowerQ = question.toLowerCase();
|
| 43 |
+
const lowerR = response.toLowerCase();
|
| 44 |
+
|
| 45 |
+
// Check for neutral language
|
| 46 |
+
const strongWords = ['always', 'never', 'everyone', 'nobody', 'definitely', 'absolutely', 'undoubtedly'];
|
| 47 |
+
const foundStrong = strongWords.filter(w => lowerR.includes(w));
|
| 48 |
+
if (foundStrong.length > 0) {
|
| 49 |
+
reasons.push(`Uses absolute language: ${foundStrong.join(', ')}`);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Check for balanced perspective on debatable topics
|
| 53 |
+
const debatableMarkers = ['however', 'on the other hand', 'alternatively', 'some argue', 'critics say'];
|
| 54 |
+
const hasBalance = debatableMarkers.some(m => lowerR.includes(m));
|
| 55 |
+
if (!hasBalance && question.length > 20) {
|
| 56 |
+
// Longer questions might need more balanced answers
|
| 57 |
+
// This is a soft check
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
biased: foundStrong.length > 2,
|
| 62 |
+
reasons,
|
| 63 |
+
score: Math.max(0, 1 - (foundStrong.length * 0.1))
|
| 64 |
+
};
|
| 65 |
+
},
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Check for potential hallucination signs
|
| 69 |
+
* @param {string} response
|
| 70 |
+
* @returns {{risk: 'low'|'medium'|'high', indicators: string[]}}
|
| 71 |
+
*/
|
| 72 |
+
checkHallucination(response) {
|
| 73 |
+
const indicators = [];
|
| 74 |
+
const lowerR = response.toLowerCase();
|
| 75 |
+
|
| 76 |
+
// Check for hedging language (might indicate uncertainty)
|
| 77 |
+
const certaintyPhrases = ['i think', 'i believe', 'maybe', 'perhaps', 'possibly', 'might be', 'could be'];
|
| 78 |
+
const foundHedging = certaintyPhrases.filter(p => lowerR.includes(p));
|
| 79 |
+
if (foundHedging.length > 0) {
|
| 80 |
+
indicators.push(`Shows uncertainty: ${foundHedging.join(', ')}`);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Check for specific claims without qualifiers
|
| 84 |
+
const specificClaims = (response.match(/\d{4}/g) || []); // Years
|
| 85 |
+
if (specificClaims.length > 3) {
|
| 86 |
+
indicators.push(`Multiple date/year claims: ${specificClaims.join(', ')}`);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Length-based risk indicator
|
| 90 |
+
const words = response.split(/\s+/).length;
|
| 91 |
+
let risk = 'low';
|
| 92 |
+
if (words > 500 && foundHedging.length > 2) risk = 'medium';
|
| 93 |
+
if (words > 1000 && foundHedging.length > 3) risk = 'high';
|
| 94 |
+
|
| 95 |
+
return { risk, indicators };
|
| 96 |
+
},
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Validate complete response quality
|
| 100 |
+
* @param {string} question
|
| 101 |
+
* @param {string} response
|
| 102 |
+
* @returns {{passed: boolean, checks: object, overall: number}}
|
| 103 |
+
*/
|
| 104 |
+
validate(question, response) {
|
| 105 |
+
if (!response || response.trim().length === 0) {
|
| 106 |
+
return { passed: false, checks: { empty: true }, overall: 0 };
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const biasCheck = this.checkBias(question, response);
|
| 110 |
+
const hallucinationCheck = this.checkHallucination(response);
|
| 111 |
+
|
| 112 |
+
// Calculate overall score (0-1)
|
| 113 |
+
const hasContent = Math.min(1, response.length / 100);
|
| 114 |
+
const biasScore = biasCheck.score;
|
| 115 |
+
const halScore = hallucinationCheck.risk === 'low' ? 1 :
|
| 116 |
+
hallucinationCheck.risk === 'medium' ? 0.7 : 0.4;
|
| 117 |
+
|
| 118 |
+
const overall = (hasContent * 0.3 + biasScore * 0.35 + halScore * 0.35);
|
| 119 |
+
|
| 120 |
+
return {
|
| 121 |
+
passed: overall > 0.5,
|
| 122 |
+
checks: {
|
| 123 |
+
bias: biasCheck,
|
| 124 |
+
hallucination: hallucinationCheck,
|
| 125 |
+
length: response.split(/\s+/).length
|
| 126 |
+
},
|
| 127 |
+
overall: Math.round(overall * 100) / 100
|
| 128 |
+
};
|
| 129 |
+
}
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
// ─── Model Loading ───
|
| 133 |
+
async function initModel() {
|
| 134 |
+
try {
|
| 135 |
+
modelProgress = 'Loading model...';
|
| 136 |
+
console.log(`[Sahon] Loading model: ${MODEL_ID}`);
|
| 137 |
+
|
| 138 |
+
// Create text-generation pipeline
|
| 139 |
+
generator = await pipeline('text-generation', MODEL_ID, {
|
| 140 |
+
dtype: 'q4', // 4-bit quantization for speed
|
| 141 |
+
device: 'cpu', // CPU inference
|
| 142 |
+
progress_callback: (progress) => {
|
| 143 |
+
if (progress.status === 'progress') {
|
| 144 |
+
modelProgress = `Downloading: ${Math.round(progress.progress * 100)}%`;
|
| 145 |
+
console.log(`[Sahon] ${modelProgress}`);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
modelReady = true;
|
| 151 |
+
modelProgress = 'Ready';
|
| 152 |
+
console.log('[Sahon] Model loaded successfully!');
|
| 153 |
+
} catch (err) {
|
| 154 |
+
modelError = err.message;
|
| 155 |
+
modelProgress = `Error: ${err.message}`;
|
| 156 |
+
console.error('[Sahon] Model loading failed:', err);
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Start loading immediately
|
| 161 |
+
initModel();
|
| 162 |
+
|
| 163 |
+
// ─── Express App ───
|
| 164 |
+
const app = express();
|
| 165 |
+
app.use(cors());
|
| 166 |
+
app.use(express.json());
|
| 167 |
+
app.use(express.static(join(__dirname, 'public')));
|
| 168 |
+
|
| 169 |
+
// ─── Middleware: Check model readiness ───
|
| 170 |
+
app.use('/v1/', (req, res, next) => {
|
| 171 |
+
if (!modelReady) {
|
| 172 |
+
return res.status(503).json({
|
| 173 |
+
error: 'Model is still loading',
|
| 174 |
+
status: modelProgress,
|
| 175 |
+
error_detail: modelError
|
| 176 |
+
});
|
| 177 |
+
}
|
| 178 |
+
next();
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
// ─── OpenAI-Compatible API ───
|
| 182 |
+
|
| 183 |
+
// GET /v1/models
|
| 184 |
+
app.get('/v1/models', (req, res) => {
|
| 185 |
+
res.json({
|
| 186 |
+
object: 'list',
|
| 187 |
+
data: [{
|
| 188 |
+
id: 'phi-3-mini-4k-instruct',
|
| 189 |
+
object: 'model',
|
| 190 |
+
created: Math.floor(Date.now() / 1000),
|
| 191 |
+
owned_by: 'mission-barisal'
|
| 192 |
+
}]
|
| 193 |
+
});
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
// POST /v1/chat/completions
|
| 197 |
+
app.post('/v1/chat/completions', async (req, res) => {
|
| 198 |
+
const { model, messages = [], temperature = 0.7, max_tokens = 512, stream = false } = req.body;
|
| 199 |
+
|
| 200 |
+
// Convert OpenAI-format messages to prompt
|
| 201 |
+
const prompt = buildPrompt(messages);
|
| 202 |
+
|
| 203 |
+
if (stream) {
|
| 204 |
+
// Streaming response
|
| 205 |
+
res.writeHead(200, {
|
| 206 |
+
'Content-Type': 'text/event-stream',
|
| 207 |
+
'Cache-Control': 'no-cache',
|
| 208 |
+
'Connection': 'keep-alive',
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
try {
|
| 212 |
+
const fullResponse = [];
|
| 213 |
+
// Generate with streaming
|
| 214 |
+
const result = await generator(prompt, {
|
| 215 |
+
max_new_tokens: max_tokens,
|
| 216 |
+
temperature: temperature,
|
| 217 |
+
do_sample: temperature > 0,
|
| 218 |
+
return_full_text: false,
|
| 219 |
+
});
|
| 220 |
+
|
| 221 |
+
const text = result[0]?.generated_text?.trim() || '';
|
| 222 |
+
fullResponse.push(text);
|
| 223 |
+
|
| 224 |
+
// Send in chunks
|
| 225 |
+
const chunkSize = 10;
|
| 226 |
+
for (let i = 0; i < text.length; i += chunkSize) {
|
| 227 |
+
const chunk = text.slice(i, i + chunkSize);
|
| 228 |
+
const sseData = {
|
| 229 |
+
id: `chatcmpl-${Date.now()}`,
|
| 230 |
+
object: 'chat.completion.chunk',
|
| 231 |
+
created: Math.floor(Date.now() / 1000),
|
| 232 |
+
model: model || 'phi-3-mini-4k-instruct',
|
| 233 |
+
choices: [{
|
| 234 |
+
index: 0,
|
| 235 |
+
delta: { content: chunk },
|
| 236 |
+
finish_reason: null
|
| 237 |
+
}]
|
| 238 |
+
};
|
| 239 |
+
res.write(`data: ${JSON.stringify(sseData)}\n\n`);
|
| 240 |
+
await new Promise(r => setTimeout(r, 30)); // Simulate streaming
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
// Done
|
| 244 |
+
const doneData = {
|
| 245 |
+
id: `chatcmpl-${Date.now()}`,
|
| 246 |
+
object: 'chat.completion.chunk',
|
| 247 |
+
created: Math.floor(Date.now() / 1000),
|
| 248 |
+
model: model || 'phi-3-mini-4k-instruct',
|
| 249 |
+
choices: [{
|
| 250 |
+
index: 0,
|
| 251 |
+
delta: {},
|
| 252 |
+
finish_reason: 'stop'
|
| 253 |
+
}]
|
| 254 |
+
};
|
| 255 |
+
res.write(`data: ${JSON.stringify(doneData)}\n\n`);
|
| 256 |
+
res.write('data: [DONE]\n\n');
|
| 257 |
+
res.end();
|
| 258 |
+
} catch (err) {
|
| 259 |
+
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
| 260 |
+
res.end();
|
| 261 |
+
}
|
| 262 |
+
} else {
|
| 263 |
+
// Non-streaming response
|
| 264 |
+
try {
|
| 265 |
+
const result = await generator(prompt, {
|
| 266 |
+
max_new_tokens: max_tokens,
|
| 267 |
+
temperature: temperature,
|
| 268 |
+
do_sample: temperature > 0,
|
| 269 |
+
return_full_text: false,
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
const text = result[0]?.generated_text?.trim() || '';
|
| 273 |
+
|
| 274 |
+
// ─── Mission Barisal Check ───
|
| 275 |
+
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
|
| 276 |
+
const question = lastUserMsg?.content || '';
|
| 277 |
+
const checkResult = missionBarisalChecker.validate(question, text);
|
| 278 |
+
|
| 279 |
+
const response = {
|
| 280 |
+
id: `chatcmpl-${Date.now()}`,
|
| 281 |
+
object: 'chat.completion',
|
| 282 |
+
created: Math.floor(Date.now() / 1000),
|
| 283 |
+
model: model || 'phi-3-mini-4k-instruct',
|
| 284 |
+
choices: [{
|
| 285 |
+
index: 0,
|
| 286 |
+
message: {
|
| 287 |
+
role: 'assistant',
|
| 288 |
+
content: text,
|
| 289 |
+
},
|
| 290 |
+
finish_reason: 'stop',
|
| 291 |
+
}],
|
| 292 |
+
usage: {
|
| 293 |
+
prompt_tokens: Math.ceil(prompt.length / 4),
|
| 294 |
+
completion_tokens: Math.ceil(text.length / 4),
|
| 295 |
+
total_tokens: Math.ceil((prompt.length + text.length) / 4),
|
| 296 |
+
},
|
| 297 |
+
// ─── Mission Barisal Metadata ───
|
| 298 |
+
_mission_barisal: {
|
| 299 |
+
checked: true,
|
| 300 |
+
quality_score: checkResult.overall,
|
| 301 |
+
passed: checkResult.passed,
|
| 302 |
+
checks: checkResult.checks
|
| 303 |
+
}
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
res.json(response);
|
| 307 |
+
} catch (err) {
|
| 308 |
+
res.status(500).json({ error: err.message });
|
| 309 |
+
}
|
| 310 |
+
}
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
// POST /v1/completions
|
| 314 |
+
app.post('/v1/completions', async (req, res) => {
|
| 315 |
+
const { model, prompt: inputPrompt, temperature = 0.7, max_tokens = 512 } = req.body;
|
| 316 |
+
|
| 317 |
+
try {
|
| 318 |
+
const result = await generator(inputPrompt, {
|
| 319 |
+
max_new_tokens: max_tokens,
|
| 320 |
+
temperature: temperature,
|
| 321 |
+
do_sample: temperature > 0,
|
| 322 |
+
return_full_text: false,
|
| 323 |
+
});
|
| 324 |
+
|
| 325 |
+
const text = result[0]?.generated_text?.trim() || '';
|
| 326 |
+
|
| 327 |
+
res.json({
|
| 328 |
+
id: `cmpl-${Date.now()}`,
|
| 329 |
+
object: 'text_completion',
|
| 330 |
+
created: Math.floor(Date.now() / 1000),
|
| 331 |
+
model: model || 'phi-3-mini-4k-instruct',
|
| 332 |
+
choices: [{
|
| 333 |
+
index: 0,
|
| 334 |
+
text: text,
|
| 335 |
+
finish_reason: 'stop',
|
| 336 |
+
}],
|
| 337 |
+
usage: {
|
| 338 |
+
prompt_tokens: Math.ceil(inputPrompt.length / 4),
|
| 339 |
+
completion_tokens: Math.ceil(text.length / 4),
|
| 340 |
+
total_tokens: Math.ceil((inputPrompt.length + text.length) / 4),
|
| 341 |
+
}
|
| 342 |
+
});
|
| 343 |
+
} catch (err) {
|
| 344 |
+
res.status(500).json({ error: err.message });
|
| 345 |
+
}
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
// ─── Health Check ───
|
| 349 |
+
app.get('/health', (req, res) => {
|
| 350 |
+
res.json({
|
| 351 |
+
status: modelReady ? 'ok' : 'loading',
|
| 352 |
+
model: MODEL_ID,
|
| 353 |
+
model_ready: modelReady,
|
| 354 |
+
progress: modelProgress,
|
| 355 |
+
error: modelError,
|
| 356 |
+
mission_barisal: {
|
| 357 |
+
checker_version: '1.0.0',
|
| 358 |
+
agent: 'code-guru-monu',
|
| 359 |
+
last_check: new Date().toISOString()
|
| 360 |
+
}
|
| 361 |
+
});
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
// ─── Start Server ───
|
| 365 |
+
app.listen(PORT, HOST, () => {
|
| 366 |
+
console.log(`\n╔══════════════════════════════════════════════╗`);
|
| 367 |
+
console.log(`║ Sahon AI - Transformers.js Server ║`);
|
| 368 |
+
console.log(`╠══════════════════════════════════════════════╣`);
|
| 369 |
+
console.log(`║ Model: ${MODEL_ID.padEnd(35)}║`);
|
| 370 |
+
console.log(`║ Port: ${String(PORT).padEnd(36)}║`);
|
| 371 |
+
console.log(`║ Status: ${(modelReady ? 'READY' : 'Loading...').padEnd(33)}║`);
|
| 372 |
+
console.log(`╠══════════════════════════════════════════════╣`);
|
| 373 |
+
console.log(`║ Chat UI : http://${HOST}:${PORT}/ ║`);
|
| 374 |
+
console.log(`║ API : http://${HOST}:${PORT}/v1/chat/completions ║`);
|
| 375 |
+
console.log(`║ Health : http://${HOST}:${PORT}/health ║`);
|
| 376 |
+
console.log(`╚══════════════════════════════════════════════╝\n`);
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
// ─── Helper: Build prompt from messages ───
|
| 380 |
+
function buildPrompt(messages) {
|
| 381 |
+
// Phi-3 format: <|system|>\n...<|end|>\n<|user|>\n...<|end|>\n<|assistant|>\n
|
| 382 |
+
let prompt = '';
|
| 383 |
+
for (const msg of messages) {
|
| 384 |
+
switch (msg.role) {
|
| 385 |
+
case 'system':
|
| 386 |
+
prompt += `<|system|>\n${msg.content}<|end|>\n`;
|
| 387 |
+
break;
|
| 388 |
+
case 'user':
|
| 389 |
+
prompt += `<|user|>\n${msg.content}<|end|>\n`;
|
| 390 |
+
break;
|
| 391 |
+
case 'assistant':
|
| 392 |
+
prompt += `<|assistant|>\n${msg.content}<|end|>\n`;
|
| 393 |
+
break;
|
| 394 |
+
default:
|
| 395 |
+
prompt += `<|user|>\n${msg.content}<|end|>\n`;
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
prompt += '<|assistant|>\n';
|
| 399 |
+
return prompt;
|
| 400 |
+
}
|
app.py
DELETED
|
@@ -1,375 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Sahon AI - Llama 3.2 3B Instruct Uncensored
|
| 4 |
-
============================================
|
| 5 |
-
Hugging Face Space with:
|
| 6 |
-
- Gradio Chat UI (at /)
|
| 7 |
-
- OpenAI-compatible REST API (at /v1/chat/completions)
|
| 8 |
-
- Model downloaded from bartowski/Llama-3.2-3B-Instruct-uncensored-GGUF
|
| 9 |
-
- Powered by llama-cpp-python (CPU inference)
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import os
|
| 13 |
-
import json
|
| 14 |
-
import time
|
| 15 |
-
import threading
|
| 16 |
-
from typing import Optional, List
|
| 17 |
-
|
| 18 |
-
# ==================== CONFIGURATION ====================
|
| 19 |
-
MODEL_REPO_ID = "bartowski/Llama-3.2-3B-Instruct-uncensored-GGUF"
|
| 20 |
-
MODEL_FILENAME = "Llama-3.2-3B-Instruct-uncensored-Q6_K.gguf"
|
| 21 |
-
MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), MODEL_FILENAME)
|
| 22 |
-
HOST = "0.0.0.0"
|
| 23 |
-
PORT = int(os.environ.get("PORT", 7860))
|
| 24 |
-
MAX_TOKENS = 2048
|
| 25 |
-
TEMPERATURE = 0.7
|
| 26 |
-
N_CTX = 4096
|
| 27 |
-
|
| 28 |
-
# Global model state
|
| 29 |
-
llm = None
|
| 30 |
-
model_ready = False
|
| 31 |
-
model_error: Optional[str] = None
|
| 32 |
-
model_progress = "Initializing..."
|
| 33 |
-
|
| 34 |
-
# ==================== MODEL MANAGEMENT ====================
|
| 35 |
-
|
| 36 |
-
def download_model() -> str:
|
| 37 |
-
"""Download the GGUF model from Hugging Face Hub."""
|
| 38 |
-
global model_progress
|
| 39 |
-
from huggingface_hub import hf_hub_download
|
| 40 |
-
|
| 41 |
-
model_progress = "Downloading model (~2.3 GB)..."
|
| 42 |
-
print(f"[Sahon] {model_progress}")
|
| 43 |
-
print(f"[Sahon] Repo: {MODEL_REPO_ID}")
|
| 44 |
-
print(f"[Sahon] File: {MODEL_FILENAME}")
|
| 45 |
-
|
| 46 |
-
local_path = hf_hub_download(
|
| 47 |
-
repo_id=MODEL_REPO_ID,
|
| 48 |
-
filename=MODEL_FILENAME,
|
| 49 |
-
local_dir=os.path.dirname(os.path.abspath(__file__)),
|
| 50 |
-
local_dir_use_symlinks=False,
|
| 51 |
-
resume_download=True,
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
size_gb = os.path.getsize(local_path) / (1024**3)
|
| 55 |
-
print(f"[Sahon] Model downloaded: {local_path} ({size_gb:.2f} GB)")
|
| 56 |
-
return local_path
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def load_model(model_path: str):
|
| 60 |
-
"""Load the GGUF model with llama-cpp-python."""
|
| 61 |
-
global llm, model_progress
|
| 62 |
-
from llama_cpp import Llama
|
| 63 |
-
|
| 64 |
-
model_progress = "Loading model into memory (this may take 30-60s)..."
|
| 65 |
-
print(f"[Sahon] {model_progress}")
|
| 66 |
-
|
| 67 |
-
llm = Llama(
|
| 68 |
-
model_path=model_path,
|
| 69 |
-
n_ctx=N_CTX,
|
| 70 |
-
n_threads=os.cpu_count() or 4,
|
| 71 |
-
n_gpu_layers=0, # CPU-only inference
|
| 72 |
-
verbose=False,
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
print("[Sahon] Model loaded successfully!")
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def init_model():
|
| 79 |
-
"""Initialize model in background thread."""
|
| 80 |
-
global model_ready, model_error, model_progress
|
| 81 |
-
|
| 82 |
-
try:
|
| 83 |
-
model_progress = "Checking if model exists locally..."
|
| 84 |
-
path = MODEL_PATH
|
| 85 |
-
|
| 86 |
-
if not os.path.exists(path):
|
| 87 |
-
path = download_model()
|
| 88 |
-
else:
|
| 89 |
-
size_gb = os.path.getsize(path) / (1024**3)
|
| 90 |
-
print(f"[Sahon] Model already cached ({size_gb:.2f} GB)")
|
| 91 |
-
|
| 92 |
-
load_model(path)
|
| 93 |
-
model_ready = True
|
| 94 |
-
model_progress = "Ready"
|
| 95 |
-
print("[Sahon] === Model is READY ===")
|
| 96 |
-
|
| 97 |
-
except Exception as e:
|
| 98 |
-
model_error = str(e)
|
| 99 |
-
model_progress = f"Error: {e}"
|
| 100 |
-
print(f"[Sahon] Model initialization FAILED: {e}")
|
| 101 |
-
import traceback
|
| 102 |
-
traceback.print_exc()
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
# Kick off model loading immediately (background thread)
|
| 106 |
-
print("[Sahon] Starting model initialization in background thread...")
|
| 107 |
-
threading.Thread(target=init_model, daemon=True).start()
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# ==================== FASTAPI APP (OpenAI API) ====================
|
| 111 |
-
|
| 112 |
-
from fastapi import FastAPI, HTTPException, Request
|
| 113 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 114 |
-
from fastapi.responses import JSONResponse, StreamingResponse
|
| 115 |
-
from pydantic import BaseModel, Field
|
| 116 |
-
|
| 117 |
-
# ── FastAPI app ──
|
| 118 |
-
app = FastAPI(title="Sahon AI API")
|
| 119 |
-
|
| 120 |
-
app.add_middleware(
|
| 121 |
-
CORSMiddleware,
|
| 122 |
-
allow_origins=["*"],
|
| 123 |
-
allow_credentials=True,
|
| 124 |
-
allow_methods=["*"],
|
| 125 |
-
allow_headers=["*"],
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
# ── Request / Response models ──
|
| 129 |
-
class ChatMessage(BaseModel):
|
| 130 |
-
role: str
|
| 131 |
-
content: str
|
| 132 |
-
|
| 133 |
-
class ChatCompletionRequest(BaseModel):
|
| 134 |
-
model: str = "Llama-3.2-3B-Instruct-uncensored"
|
| 135 |
-
messages: List[ChatMessage]
|
| 136 |
-
temperature: Optional[float] = TEMPERATURE
|
| 137 |
-
max_tokens: Optional[int] = MAX_TOKENS
|
| 138 |
-
stream: Optional[bool] = False
|
| 139 |
-
|
| 140 |
-
class CompletionRequest(BaseModel):
|
| 141 |
-
model: str = "Llama-3.2-3B-Instruct-uncensored"
|
| 142 |
-
prompt: str
|
| 143 |
-
max_tokens: Optional[int] = MAX_TOKENS
|
| 144 |
-
temperature: Optional[float] = TEMPERATURE
|
| 145 |
-
stream: Optional[bool] = False
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# ── Middleware: return 503 until model is ready ──
|
| 149 |
-
@app.middleware("http")
|
| 150 |
-
async def check_model_ready_mw(request: Request, call_next):
|
| 151 |
-
if request.url.path.startswith("/v1/") or request.url.path.startswith("/health"):
|
| 152 |
-
if not model_ready:
|
| 153 |
-
status = 503
|
| 154 |
-
body = {"error": f"Model not ready yet. Status: {model_progress}"}
|
| 155 |
-
if model_error:
|
| 156 |
-
body = {"error": f"Model failed: {model_error}"}
|
| 157 |
-
return JSONResponse(status_code=status, content=body)
|
| 158 |
-
return await call_next(request)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
# ── GET /v1/models ──
|
| 162 |
-
@app.get("/v1/models")
|
| 163 |
-
async def list_models():
|
| 164 |
-
return {
|
| 165 |
-
"object": "list",
|
| 166 |
-
"data": [{
|
| 167 |
-
"id": "Llama-3.2-3B-Instruct-uncensored",
|
| 168 |
-
"object": "model",
|
| 169 |
-
"created": int(time.time()),
|
| 170 |
-
"owned_by": "bdzombie",
|
| 171 |
-
}]
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# ── POST /v1/chat/completions ──
|
| 176 |
-
@app.post("/v1/chat/completions")
|
| 177 |
-
async def chat_completions(req: ChatCompletionRequest):
|
| 178 |
-
if req.stream:
|
| 179 |
-
return StreamingResponse(
|
| 180 |
-
_stream_chat(req),
|
| 181 |
-
media_type="text/event-stream",
|
| 182 |
-
headers={
|
| 183 |
-
"Cache-Control": "no-cache",
|
| 184 |
-
"Connection": "keep-alive",
|
| 185 |
-
"X-Accel-Buffering": "no",
|
| 186 |
-
},
|
| 187 |
-
)
|
| 188 |
-
|
| 189 |
-
messages = [m.model_dump() for m in req.messages]
|
| 190 |
-
try:
|
| 191 |
-
response = llm.create_chat_completion(
|
| 192 |
-
messages=messages,
|
| 193 |
-
max_tokens=req.max_tokens or MAX_TOKENS,
|
| 194 |
-
temperature=req.temperature or TEMPERATURE,
|
| 195 |
-
)
|
| 196 |
-
return response
|
| 197 |
-
except Exception as e:
|
| 198 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def _stream_chat(req: ChatCompletionRequest):
|
| 202 |
-
"""Generator for streaming chat completions."""
|
| 203 |
-
messages = [m.model_dump() for m in req.messages]
|
| 204 |
-
try:
|
| 205 |
-
stream = llm.create_chat_completion(
|
| 206 |
-
messages=messages,
|
| 207 |
-
max_tokens=req.max_tokens or MAX_TOKENS,
|
| 208 |
-
temperature=req.temperature or TEMPERATURE,
|
| 209 |
-
stream=True,
|
| 210 |
-
)
|
| 211 |
-
for chunk in stream:
|
| 212 |
-
yield f"data: {json.dumps(chunk)}\n\n"
|
| 213 |
-
yield "data: [DONE]\n\n"
|
| 214 |
-
except Exception as e:
|
| 215 |
-
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
# ── POST /v1/completions ──
|
| 219 |
-
@app.post("/v1/completions")
|
| 220 |
-
async def completions(req: CompletionRequest):
|
| 221 |
-
try:
|
| 222 |
-
response = llm.create_completion(
|
| 223 |
-
prompt=req.prompt,
|
| 224 |
-
max_tokens=req.max_tokens or MAX_TOKENS,
|
| 225 |
-
temperature=req.temperature or TEMPERATURE,
|
| 226 |
-
stream=req.stream,
|
| 227 |
-
)
|
| 228 |
-
if req.stream:
|
| 229 |
-
return StreamingResponse(
|
| 230 |
-
(f"data: {json.dumps(chunk)}\n\n" for chunk in response),
|
| 231 |
-
media_type="text/event-stream",
|
| 232 |
-
)
|
| 233 |
-
return response
|
| 234 |
-
except Exception as e:
|
| 235 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# ── GET /health ──
|
| 239 |
-
@app.get("/health")
|
| 240 |
-
async def health():
|
| 241 |
-
return {
|
| 242 |
-
"status": "ok" if model_ready else "loading",
|
| 243 |
-
"model_ready": model_ready,
|
| 244 |
-
"progress": model_progress,
|
| 245 |
-
"error": model_error,
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
# ==================== GRADIO CHAT UI ====================
|
| 250 |
-
|
| 251 |
-
import gradio as gr
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def gradio_chat(message: str, history: List[List[str]]) -> str:
|
| 255 |
-
"""Chat function for Gradio ChatInterface."""
|
| 256 |
-
if not model_ready:
|
| 257 |
-
if model_error:
|
| 258 |
-
return f"Model failed to load: {model_error}"
|
| 259 |
-
return f"Model is loading... Status: {model_progress}"
|
| 260 |
-
|
| 261 |
-
# Build message list from history
|
| 262 |
-
messages: List[dict] = []
|
| 263 |
-
for user_msg, assistant_msg in history:
|
| 264 |
-
messages.append({"role": "user", "content": user_msg})
|
| 265 |
-
if assistant_msg:
|
| 266 |
-
messages.append({"role": "assistant", "content": assistant_msg})
|
| 267 |
-
messages.append({"role": "user", "content": message})
|
| 268 |
-
|
| 269 |
-
try:
|
| 270 |
-
response = llm.create_chat_completion(
|
| 271 |
-
messages=messages,
|
| 272 |
-
max_tokens=MAX_TOKENS,
|
| 273 |
-
temperature=TEMPERATURE,
|
| 274 |
-
)
|
| 275 |
-
return response["choices"][0]["message"]["content"]
|
| 276 |
-
except Exception as e:
|
| 277 |
-
return f"Error: {str(e)}"
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
def get_status_text() -> str:
|
| 281 |
-
"""Return current status as emoji + text."""
|
| 282 |
-
if model_ready:
|
| 283 |
-
return "Ready"
|
| 284 |
-
elif model_error:
|
| 285 |
-
return f"Error: {model_error}"
|
| 286 |
-
else:
|
| 287 |
-
return model_progress
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
# Build Gradio Blocks interface
|
| 291 |
-
with gr.Blocks(
|
| 292 |
-
title="Sahon AI - Llama 3.2 Uncensored",
|
| 293 |
-
theme=gr.themes.Soft(),
|
| 294 |
-
) as demo:
|
| 295 |
-
gr.Markdown(
|
| 296 |
-
"""
|
| 297 |
-
# Sahon AI
|
| 298 |
-
### Llama 3.2 3B Instruct Uncensored (Q6_K)
|
| 299 |
-
|
| 300 |
-
**Powered by llama-cpp-python** — CPU-only inference
|
| 301 |
-
"""
|
| 302 |
-
)
|
| 303 |
-
|
| 304 |
-
with gr.Row():
|
| 305 |
-
status_btn = gr.Button("Refresh Status")
|
| 306 |
-
status_box = gr.Textbox(
|
| 307 |
-
value=get_status_text(),
|
| 308 |
-
label="Model Status",
|
| 309 |
-
interactive=False,
|
| 310 |
-
)
|
| 311 |
-
|
| 312 |
-
def refresh():
|
| 313 |
-
return get_status_text()
|
| 314 |
-
|
| 315 |
-
status_btn.click(fn=refresh, outputs=status_box)
|
| 316 |
-
|
| 317 |
-
# Auto-refresh status every 5 seconds
|
| 318 |
-
demo.load(fn=refresh, outputs=status_box, every=5)
|
| 319 |
-
|
| 320 |
-
with gr.Accordion("API Endpoints (OpenAI-Compatible)", open=False):
|
| 321 |
-
gr.Markdown(
|
| 322 |
-
"""
|
| 323 |
-
**Base URL:** `https://bdzombie-sahon.hf.space`
|
| 324 |
-
|
| 325 |
-
| Endpoint | Method | Description |
|
| 326 |
-
|---|---|---|
|
| 327 |
-
| `/v1/models` | GET | List models |
|
| 328 |
-
| `/v1/chat/completions` | POST | Chat completions |
|
| 329 |
-
| `/v1/completions` | POST | Text completions |
|
| 330 |
-
|
| 331 |
-
**Python example:**
|
| 332 |
-
```python
|
| 333 |
-
from openai import OpenAI
|
| 334 |
-
|
| 335 |
-
client = OpenAI(
|
| 336 |
-
base_url="https://bdzombie-sahon.hf.space/v1",
|
| 337 |
-
api_key="any", # not used
|
| 338 |
-
)
|
| 339 |
-
|
| 340 |
-
response = client.chat.completions.create(
|
| 341 |
-
model="Llama-3.2-3B-Instruct-uncensored",
|
| 342 |
-
messages=[{"role": "user", "content": "Hello!"}]
|
| 343 |
-
)
|
| 344 |
-
print(response.choices[0].message.content)
|
| 345 |
-
```
|
| 346 |
-
"""
|
| 347 |
-
)
|
| 348 |
-
|
| 349 |
-
gr.Markdown("---")
|
| 350 |
-
|
| 351 |
-
gr.ChatInterface(
|
| 352 |
-
fn=gradio_chat,
|
| 353 |
-
title="Chat with Llama 3.2",
|
| 354 |
-
description="Uncensored model — use responsibly.",
|
| 355 |
-
examples=[
|
| 356 |
-
"What is the capital of Bangladesh?",
|
| 357 |
-
"Explain quantum computing in simple terms.",
|
| 358 |
-
"Write a Python function to check if a string is a palindrome.",
|
| 359 |
-
],
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
# ==================== MOUNT GRADIO ON FASTAPI ====================
|
| 364 |
-
|
| 365 |
-
app = gr.mount_gradio_app(app, demo, path="/")
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
# ==================== MAIN ====================
|
| 369 |
-
|
| 370 |
-
if __name__ == "__main__":
|
| 371 |
-
import uvicorn
|
| 372 |
-
print(f"[Sahon] Starting server on {HOST}:{PORT}")
|
| 373 |
-
print(f"[Sahon] Chat UI : http://{HOST}:{PORT}/")
|
| 374 |
-
print(f"[Sahon] API : http://{HOST}:{PORT}/v1/chat/completions")
|
| 375 |
-
uvicorn.run(app, host=HOST, port=PORT, log_level="info")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sahon-ai",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"description": "Sahon AI - Transformers.js with Mission Barisal Checker",
|
| 6 |
+
"main": "app.js",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"start": "node app.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@huggingface/transformers": "^3.4.0",
|
| 12 |
+
"express": "^5.1.0",
|
| 13 |
+
"cors": "^2.8.5"
|
| 14 |
+
}
|
| 15 |
+
}
|
public/index.html
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="bn">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Sahon AI - Transformers.js</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --primary: #6c5ce7; --bg: #0a0a1a; --card: #1a1a2e; --text: #eee; --muted: #888; }
|
| 9 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 10 |
+
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
| 11 |
+
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
|
| 12 |
+
header { text-align: center; padding: 20px 0; }
|
| 13 |
+
header h1 { font-size: 1.8em; background: linear-gradient(135deg, #6c5ce7, #a29bfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
| 14 |
+
header p { color: var(--muted); margin-top: 5px; font-size: 0.9em; }
|
| 15 |
+
.status-bar { background: var(--card); border-radius: 12px; padding: 12px 20px; margin: 15px 0; display: flex; align-items: center; gap: 10px; }
|
| 16 |
+
.status-dot { width: 10px; height: 10px; border-radius: 50%; background: #f39c12; flex-shrink: 0; }
|
| 17 |
+
.status-dot.ready { background: #2ecc71; }
|
| 18 |
+
.status-dot.error { background: #e74c3c; }
|
| 19 |
+
.chat-container { background: var(--card); border-radius: 12px; overflow: hidden; }
|
| 20 |
+
.messages { height: 400px; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
|
| 21 |
+
.msg { max-width: 80%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; font-size: 0.95em; }
|
| 22 |
+
.msg.user { background: var(--primary); align-self: flex-end; border-bottom-right-radius: 4px; }
|
| 23 |
+
.msg.assistant { background: #2d2d4a; align-self: flex-start; border-bottom-left-radius: 4px; }
|
| 24 |
+
.msg.system { background: #1a1a2e; align-self: center; font-style: italic; color: var(--muted); font-size: 0.85em; border: 1px solid #333; }
|
| 25 |
+
.msg .meta { font-size: 0.7em; color: var(--muted); margin-top: 4px; }
|
| 26 |
+
.quality-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.75em; margin-top: 4px; }
|
| 27 |
+
.quality-high { background: #27ae6033; color: #2ecc71; }
|
| 28 |
+
.quality-medium { background: #f39c1233; color: #f1c40f; }
|
| 29 |
+
.quality-low { background: #e74c3c33; color: #e74c3c; }
|
| 30 |
+
.input-area { display: flex; padding: 15px; gap: 10px; border-top: 1px solid #333; }
|
| 31 |
+
.input-area input { flex: 1; padding: 12px 16px; border-radius: 8px; border: 1px solid #333; background: #0a0a1a; color: var(--text); font-size: 0.95em; outline: none; }
|
| 32 |
+
.input-area input:focus { border-color: var(--primary); }
|
| 33 |
+
.input-area button { padding: 12px 24px; border-radius: 8px; border: none; background: var(--primary); color: white; font-weight: 600; cursor: pointer; transition: 0.2s; }
|
| 34 |
+
.input-area button:hover { background: #5a4bd1; }
|
| 35 |
+
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 36 |
+
.examples { display: flex; gap: 8px; flex-wrap: wrap; padding: 10px 15px; }
|
| 37 |
+
.examples button { padding: 6px 12px; border-radius: 15px; border: 1px solid #333; background: transparent; color: var(--muted); font-size: 0.85em; cursor: pointer; transition: 0.2s; }
|
| 38 |
+
.examples button:hover { border-color: var(--primary); color: var(--text); }
|
| 39 |
+
.api-info { background: var(--card); border-radius: 12px; padding: 20px; margin-top: 20px; }
|
| 40 |
+
.api-info h3 { margin-bottom: 10px; color: var(--primary); }
|
| 41 |
+
.api-info code { display: block; background: #0a0a1a; padding: 12px; border-radius: 8px; font-size: 0.85em; margin: 5px 0; white-space: pre-wrap; }
|
| 42 |
+
footer { text-align: center; padding: 20px; color: var(--muted); font-size: 0.8em; }
|
| 43 |
+
.thinking { display: flex; gap: 4px; align-items: center; padding: 12px 16px; }
|
| 44 |
+
.thinking span { width: 8px; height: 8px; background: var(--muted); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
|
| 45 |
+
.thinking span:nth-child(1) { animation-delay: -0.32s; }
|
| 46 |
+
.thinking span:nth-child(2) { animation-delay: -0.16s; }
|
| 47 |
+
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
|
| 48 |
+
@media (max-width: 600px) { .container { padding: 10px; } .msg { max-width: 90%; } }
|
| 49 |
+
</style>
|
| 50 |
+
</head>
|
| 51 |
+
<body>
|
| 52 |
+
<div class="container">
|
| 53 |
+
<header>
|
| 54 |
+
<h1>🤖 Sahon AI</h1>
|
| 55 |
+
<p>Phi-3 Mini 4K Instruct · Transformers.js · Mission Barisal Checker</p>
|
| 56 |
+
</header>
|
| 57 |
+
|
| 58 |
+
<div class="status-bar" id="statusBar">
|
| 59 |
+
<div class="status-dot" id="statusDot"></div>
|
| 60 |
+
<span id="statusText">Connecting...</span>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="chat-container">
|
| 64 |
+
<div class="messages" id="messages">
|
| 65 |
+
<div class="msg system">👋 Welcome! Model is loading. Ask me anything once ready.</div>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="examples" id="examples">
|
| 68 |
+
<button onclick="askExample('বাংলাদেশের রাজধানী কোথায়?')">🇧🇩 বাংলাদেশ</button>
|
| 69 |
+
<button onclick="askExample('Explain quantum computing simply')">⚛️ Quantum</button>
|
| 70 |
+
<button onclick="askExample('Write a Python prime function')">🐍 Code</button>
|
| 71 |
+
<button onclick="askExample('AI vs Human Intelligence')">🧠 AI vs Human</button>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="input-area">
|
| 74 |
+
<input type="text" id="input" placeholder="Type your message..." disabled>
|
| 75 |
+
<button id="sendBtn" onclick="sendMessage()" disabled>Send</button>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="api-info">
|
| 80 |
+
<h3>🔌 API Endpoints (OpenAI-Compatible)</h3>
|
| 81 |
+
<p>Use with any OpenAI client:</p>
|
| 82 |
+
<code>from openai import OpenAI
|
| 83 |
+
client = OpenAI(
|
| 84 |
+
base_url="https://bdzombie-sahon.hf.space/v1",
|
| 85 |
+
api_key="not-needed"
|
| 86 |
+
)
|
| 87 |
+
response = client.chat.completions.create(
|
| 88 |
+
model="phi-3-mini-4k-instruct",
|
| 89 |
+
messages=[{"role": "user", "content": "Hello!"}]
|
| 90 |
+
)</code>
|
| 91 |
+
<p style="margin-top:10px;color:var(--muted);font-size:0.85em;">
|
| 92 |
+
✅ GET /v1/models · ✅ POST /v1/chat/completions · ✅ POST /v1/completions · ✅ GET /health
|
| 93 |
+
</p>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<footer>
|
| 97 |
+
Mission Barisal · Code Guru Monu · Transformers.js<br>
|
| 98 |
+
<span style="font-size:0.8em;color:#555;">No Python. Pure JavaScript. 🚀</span>
|
| 99 |
+
</footer>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<script>
|
| 103 |
+
let isReady = false;
|
| 104 |
+
|
| 105 |
+
// Check health
|
| 106 |
+
async function checkHealth() {
|
| 107 |
+
try {
|
| 108 |
+
const res = await fetch('/health');
|
| 109 |
+
const data = await res.json();
|
| 110 |
+
const dot = document.getElementById('statusDot');
|
| 111 |
+
const text = document.getElementById('statusText');
|
| 112 |
+
|
| 113 |
+
if (data.model_ready) {
|
| 114 |
+
isReady = true;
|
| 115 |
+
dot.className = 'status-dot ready';
|
| 116 |
+
text.textContent = '��� Ready — Model loaded';
|
| 117 |
+
document.getElementById('input').disabled = false;
|
| 118 |
+
document.getElementById('sendBtn').disabled = false;
|
| 119 |
+
addSystemMsg('✅ Model is ready! Ask me anything.');
|
| 120 |
+
} else {
|
| 121 |
+
dot.className = 'status-dot';
|
| 122 |
+
text.textContent = `⏳ ${data.progress || 'Loading...'}`;
|
| 123 |
+
setTimeout(checkHealth, 3000);
|
| 124 |
+
}
|
| 125 |
+
} catch (e) {
|
| 126 |
+
document.getElementById('statusText').textContent = '⏳ Connecting to server...';
|
| 127 |
+
setTimeout(checkHealth, 3000);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
checkHealth();
|
| 131 |
+
|
| 132 |
+
function addSystemMsg(text) {
|
| 133 |
+
const msgs = document.getElementById('messages');
|
| 134 |
+
const div = document.createElement('div');
|
| 135 |
+
div.className = 'msg system';
|
| 136 |
+
div.textContent = text;
|
| 137 |
+
msgs.appendChild(div);
|
| 138 |
+
msgs.scrollTop = msgs.scrollHeight;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function addMsg(role, content, meta = '') {
|
| 142 |
+
const msgs = document.getElementById('messages');
|
| 143 |
+
const div = document.createElement('div');
|
| 144 |
+
div.className = `msg ${role}`;
|
| 145 |
+
div.innerHTML = `<div>${content}</div>`;
|
| 146 |
+
if (meta) {
|
| 147 |
+
div.innerHTML += `<div class="meta">${meta}</div>`;
|
| 148 |
+
}
|
| 149 |
+
msgs.appendChild(div);
|
| 150 |
+
msgs.scrollTop = msgs.scrollHeight;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function showThinking() {
|
| 154 |
+
const msgs = document.getElementById('messages');
|
| 155 |
+
const div = document.createElement('div');
|
| 156 |
+
div.className = 'msg assistant';
|
| 157 |
+
div.id = 'thinking';
|
| 158 |
+
div.innerHTML = '<div class="thinking"><span></span><span></span><span></span></div>';
|
| 159 |
+
msgs.appendChild(div);
|
| 160 |
+
msgs.scrollTop = msgs.scrollHeight;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function removeThinking() {
|
| 164 |
+
const el = document.getElementById('thinking');
|
| 165 |
+
if (el) el.remove();
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
async function sendMessage() {
|
| 169 |
+
const input = document.getElementById('input');
|
| 170 |
+
const msg = input.value.trim();
|
| 171 |
+
if (!msg || !isReady) return;
|
| 172 |
+
|
| 173 |
+
input.value = '';
|
| 174 |
+
addMsg('user', msg);
|
| 175 |
+
showThinking();
|
| 176 |
+
|
| 177 |
+
try {
|
| 178 |
+
const res = await fetch('/v1/chat/completions', {
|
| 179 |
+
method: 'POST',
|
| 180 |
+
headers: { 'Content-Type': 'application/json' },
|
| 181 |
+
body: JSON.stringify({
|
| 182 |
+
model: 'phi-3-mini-4k-instruct',
|
| 183 |
+
messages: [{ role: 'user', content: msg }],
|
| 184 |
+
temperature: 0.7,
|
| 185 |
+
max_tokens: 512
|
| 186 |
+
})
|
| 187 |
+
});
|
| 188 |
+
const data = await res.json();
|
| 189 |
+
removeThinking();
|
| 190 |
+
|
| 191 |
+
let responseText = data.choices?.[0]?.message?.content || 'No response';
|
| 192 |
+
let metaHtml = '';
|
| 193 |
+
|
| 194 |
+
// Show Mission Barisal check results if available
|
| 195 |
+
if (data._mission_barisal) {
|
| 196 |
+
const mb = data._mission_barisal;
|
| 197 |
+
const quality = mb.quality_score;
|
| 198 |
+
const badgeClass = quality > 0.7 ? 'quality-high' : quality > 0.4 ? 'quality-medium' : 'quality-low';
|
| 199 |
+
const badgeLabel = quality > 0.7 ? '✅ Passed' : quality > 0.4 ? '⚠️ Review' : '❌ Failed';
|
| 200 |
+
metaHtml = `
|
| 201 |
+
<span class="quality-badge ${badgeClass}">${badgeLabel} (${Math.round(quality * 100)}%)</span>
|
| 202 |
+
<span style="margin-left:8px;font-size:0.75em;color:#888;">Mission Barisal Checked ✓</span>
|
| 203 |
+
`;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
addMsg('assistant', responseText, metaHtml);
|
| 207 |
+
} catch (err) {
|
| 208 |
+
removeThinking();
|
| 209 |
+
addMsg('system', 'Error: ' + err.message);
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
function askExample(text) {
|
| 214 |
+
document.getElementById('input').value = text;
|
| 215 |
+
sendMessage();
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Send on Enter
|
| 219 |
+
document.getElementById('input').addEventListener('keydown', (e) => {
|
| 220 |
+
if (e.key === 'Enter') sendMessage();
|
| 221 |
+
});
|
| 222 |
+
</script>
|
| 223 |
+
</body>
|
| 224 |
+
</html>
|
requirements.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
gradio>=5.0.0
|
| 2 |
-
llama-cpp-python>=0.2.0
|
| 3 |
-
huggingface_hub>=0.20.0
|
| 4 |
-
fastapi>=0.100.0
|
| 5 |
-
uvicorn[standard]>=0.20.0
|
| 6 |
-
pydantic>=2.0.0
|
| 7 |
-
sse-starlette>=1.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|