Spaces:
Running
Running
Update script.js
Browse files
script.js
CHANGED
|
@@ -1,329 +1,327 @@
|
|
| 1 |
-
class SketchToRenderApp {
|
| 2 |
-
constructor() {
|
| 3 |
-
this.canvas = document.getElementById('drawingCanvas');
|
| 4 |
-
this.ctx = this.canvas.getContext('2d');
|
| 5 |
-
this.isDrawing = false;
|
| 6 |
-
this.drawingHistory = [];
|
| 7 |
-
this.currentPath = [];
|
| 8 |
-
this.renderedImageUrl = null;
|
| 9 |
-
|
| 10 |
-
// API Key được nhúng trực tiếp trong code
|
| 11 |
-
this.apiKey = 'AIzaSyCinF1_Z7XXPzu_zvMzifKyPIy7i7eUBGc'; // Thay thế bằng API key thực tế
|
| 12 |
-
|
| 13 |
-
this.initializeCanvas();
|
| 14 |
-
this.setupEventListeners();
|
| 15 |
-
this.saveCanvasState();
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
initializeCanvas() {
|
| 19 |
-
this.ctx.lineCap = 'round';
|
| 20 |
-
this.ctx.lineJoin = 'round';
|
| 21 |
-
this.ctx.fillStyle = 'white';
|
| 22 |
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
setupEventListeners() {
|
| 26 |
-
// Canvas drawing events
|
| 27 |
-
this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
|
| 28 |
-
this.canvas.addEventListener('mousemove', (e) => this.draw(e));
|
| 29 |
-
this.canvas.addEventListener('mouseup', () => this.stopDrawing());
|
| 30 |
-
this.canvas.addEventListener('mouseout', () => this.stopDrawing());
|
| 31 |
-
|
| 32 |
-
// Touch events for mobile
|
| 33 |
-
this.canvas.addEventListener('touchstart', (e) => {
|
| 34 |
-
e.preventDefault();
|
| 35 |
-
const touch = e.touches[0];
|
| 36 |
-
const mouseEvent = new MouseEvent('mousedown', {
|
| 37 |
-
clientX: touch.clientX,
|
| 38 |
-
clientY: touch.clientY
|
| 39 |
-
});
|
| 40 |
-
this.canvas.dispatchEvent(mouseEvent);
|
| 41 |
-
});
|
| 42 |
-
|
| 43 |
-
this.canvas.addEventListener('touchmove', (e) => {
|
| 44 |
-
e.preventDefault();
|
| 45 |
-
const touch = e.touches[0];
|
| 46 |
-
const mouseEvent = new MouseEvent('mousemove', {
|
| 47 |
-
clientX: touch.clientX,
|
| 48 |
-
clientY: touch.clientY
|
| 49 |
-
});
|
| 50 |
-
this.canvas.dispatchEvent(mouseEvent);
|
| 51 |
-
});
|
| 52 |
-
|
| 53 |
-
this.canvas.addEventListener('touchend', (e) => {
|
| 54 |
-
e.preventDefault();
|
| 55 |
-
const mouseEvent = new MouseEvent('mouseup', {});
|
| 56 |
-
this.canvas.dispatchEvent(mouseEvent);
|
| 57 |
-
});
|
| 58 |
-
|
| 59 |
-
// Control events
|
| 60 |
-
document.getElementById('brushSize').addEventListener('input', (e) => {
|
| 61 |
-
document.getElementById('brushSizeValue').textContent = e.target.value + 'px';
|
| 62 |
-
});
|
| 63 |
-
|
| 64 |
-
document.getElementById('clearCanvas').addEventListener('click', () => {
|
| 65 |
-
this.clearCanvas();
|
| 66 |
-
});
|
| 67 |
-
|
| 68 |
-
document.getElementById('undoBtn').addEventListener('click', () => {
|
| 69 |
-
this.undo();
|
| 70 |
-
});
|
| 71 |
-
|
| 72 |
-
document.getElementById('generateBtn').addEventListener('click', () => {
|
| 73 |
-
this.generateRender();
|
| 74 |
-
});
|
| 75 |
-
|
| 76 |
-
document.getElementById('downloadBtn').addEventListener('click', () => {
|
| 77 |
-
this.downloadImage();
|
| 78 |
-
});
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
getMousePos(e) {
|
| 82 |
-
const rect = this.canvas.getBoundingClientRect();
|
| 83 |
-
return {
|
| 84 |
-
x: e.clientX - rect.left,
|
| 85 |
-
y: e.clientY - rect.top
|
| 86 |
-
};
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
startDrawing(e) {
|
| 90 |
-
this.isDrawing = true;
|
| 91 |
-
const pos = this.getMousePos(e);
|
| 92 |
-
this.currentPath = [pos];
|
| 93 |
-
|
| 94 |
-
this.ctx.strokeStyle = document.getElementById('brushColor').value;
|
| 95 |
-
this.ctx.lineWidth = document.getElementById('brushSize').value;
|
| 96 |
-
|
| 97 |
-
this.ctx.beginPath();
|
| 98 |
-
this.ctx.moveTo(pos.x, pos.y);
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
draw(e) {
|
| 102 |
-
if (!this.isDrawing) return;
|
| 103 |
-
|
| 104 |
-
const pos = this.getMousePos(e);
|
| 105 |
-
this.currentPath.push(pos);
|
| 106 |
-
|
| 107 |
-
this.ctx.lineTo(pos.x, pos.y);
|
| 108 |
-
this.ctx.stroke();
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
stopDrawing() {
|
| 112 |
-
if (this.isDrawing) {
|
| 113 |
-
this.isDrawing = false;
|
| 114 |
-
this.saveCanvasState();
|
| 115 |
-
}
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
saveCanvasState() {
|
| 119 |
-
this.drawingHistory.push(this.canvas.toDataURL());
|
| 120 |
-
if (this.drawingHistory.length > 20) {
|
| 121 |
-
this.drawingHistory.shift();
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
clearCanvas() {
|
| 126 |
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 127 |
-
this.ctx.fillStyle = 'white';
|
| 128 |
-
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 129 |
-
this.saveCanvasState();
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
undo() {
|
| 133 |
-
if (this.drawingHistory.length > 1) {
|
| 134 |
-
this.drawingHistory.pop();
|
| 135 |
-
const previousState = this.drawingHistory[this.drawingHistory.length - 1];
|
| 136 |
-
const img = new Image();
|
| 137 |
-
img.onload = () => {
|
| 138 |
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 139 |
-
this.ctx.drawImage(img, 0, 0);
|
| 140 |
-
};
|
| 141 |
-
img.src = previousState;
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
async generateRender() {
|
| 146 |
-
if (!this.apiKey || this.apiKey === 'YOUR_GEMINI_API_KEY_HERE') {
|
| 147 |
-
this.showError('Vui lòng cấu hình API Key trong source code');
|
| 148 |
-
return;
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
// Check if canvas is empty
|
| 152 |
-
if (this.isCanvasEmpty()) {
|
| 153 |
-
this.showError('Vui lòng vẽ gì đó trước khi tạo render');
|
| 154 |
-
return;
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
this.showLoading(true);
|
| 158 |
-
this.hideError();
|
| 159 |
-
|
| 160 |
-
try {
|
| 161 |
-
// Step 1: Analyze sketch with Gemini
|
| 162 |
-
const analysisResult = await this.analyzeSketchWithGemini();
|
| 163 |
-
console.log('Analysis result:', analysisResult);
|
| 164 |
-
|
| 165 |
-
// Step 2: Generate image using the description from Gemini
|
| 166 |
-
const imageUrl = await this.generateImageFromDescription(analysisResult.description);
|
| 167 |
-
|
| 168 |
-
// Step 3: Display results
|
| 169 |
-
this.displayResults(imageUrl, analysisResult.score, analysisResult.prediction);
|
| 170 |
-
|
| 171 |
-
} catch (error) {
|
| 172 |
-
console.error('Error:', error);
|
| 173 |
-
this.showError(`Lỗi: ${error.message}`);
|
| 174 |
-
} finally {
|
| 175 |
-
this.showLoading(false);
|
| 176 |
-
}
|
| 177 |
-
}
|
| 178 |
-
|
| 179 |
-
isCanvasEmpty() {
|
| 180 |
-
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
| 181 |
-
return imageData.data.every((pixel, index) => {
|
| 182 |
-
if ((index + 1) % 4 === 0) return true; // Alpha channel
|
| 183 |
-
return pixel === 255; // White pixels
|
| 184 |
-
});
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
async analyzeSketchWithGemini() {
|
| 188 |
-
const canvasDataURL = this.canvas.toDataURL('image/png');
|
| 189 |
-
const base64Data = canvasDataURL.split(',')[1];
|
| 190 |
-
|
| 191 |
-
const prompt = `Phân tích bức vẽ sketch này và cung cấp:
|
| 192 |
-
|
| 193 |
-
1. ĐIỂM SỐ: Đánh giá chất lượng và độ rõ ràng từ 1–10, với tiêu chuẩn thoải mái (hình được vẽ bởi người bình thường bằng bút đen trên canvas, không yêu cầu đường nét hoàn hảo).
|
| 194 |
-
2. DỰ ĐOÁN: Tên ngắn gọn của sự vật được vẽ (VD: "Mèo", "Ngôi nhà", "Hoa")
|
| 195 |
-
3. MÔ TẢ: Mô tả ngắn gọn để tạo ảnh render (viết bằng tiếng Anh, phong cách artistic)
|
| 196 |
-
|
| 197 |
-
Định dạng trả lời:
|
| 198 |
-
ĐIỂM: [số]
|
| 199 |
-
DỰ ĐOÁN: [tên sự vật]
|
| 200 |
-
MÔ TẢ: [mô tả ngắn gọn bằng tiếng Anh]`;
|
| 201 |
-
|
| 202 |
-
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${this.apiKey}`, {
|
| 203 |
-
method: 'POST',
|
| 204 |
-
headers: {
|
| 205 |
-
'Content-Type': 'application/json',
|
| 206 |
-
},
|
| 207 |
-
body: JSON.stringify({
|
| 208 |
-
contents: [{
|
| 209 |
-
parts: [
|
| 210 |
-
{ text: prompt },
|
| 211 |
-
{
|
| 212 |
-
inline_data: {
|
| 213 |
-
mime_type: "image/png",
|
| 214 |
-
data: base64Data
|
| 215 |
-
}
|
| 216 |
-
}
|
| 217 |
-
]
|
| 218 |
-
}]
|
| 219 |
-
})
|
| 220 |
-
});
|
| 221 |
-
|
| 222 |
-
if (!response.ok) {
|
| 223 |
-
const errorData = await response.json();
|
| 224 |
-
throw new Error(`Gemini API Error: ${errorData.error?.message || 'Unknown error'}`);
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
const data = await response.json();
|
| 228 |
-
|
| 229 |
-
if (data.candidates && data.candidates.length > 0) {
|
| 230 |
-
const result = data.candidates[0].content.parts[0].text;
|
| 231 |
-
return this.parseGeminiResponse(result);
|
| 232 |
-
} else {
|
| 233 |
-
throw new Error('Không có kết quả từ Gemini API');
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
parseGeminiResponse(text) {
|
| 238 |
-
console.log('Gemini response:', text);
|
| 239 |
-
|
| 240 |
-
// Extract score
|
| 241 |
-
const scoreMatch = text.match(/ĐIỂM[:\s]*(\d+)/i) || text.match(/score[:\s]*(\d+)/i);
|
| 242 |
-
const score = scoreMatch ? parseInt(scoreMatch[1]) : 7;
|
| 243 |
-
|
| 244 |
-
// Extract prediction
|
| 245 |
-
const predictionMatch = text.match(/DỰ ĐOÁN[:\s]*([^\n]+)/i) || text.match(/prediction[:\s]*([^\n]+)/i);
|
| 246 |
-
const prediction = predictionMatch ? predictionMatch[1].trim() : 'Không xác định';
|
| 247 |
-
|
| 248 |
-
// Extract description
|
| 249 |
-
const descriptionMatch = text.match(/MÔ TẢ[:\s]*([^]*)/i) || text.match(/description[:\s]*([^]*)/i);
|
| 250 |
-
let description = descriptionMatch ? descriptionMatch[1].trim() : text;
|
| 251 |
-
|
| 252 |
-
// If no specific description found, use the whole response
|
| 253 |
-
if (!descriptionMatch) {
|
| 254 |
-
// Try to extract the most descriptive part
|
| 255 |
-
const lines = text.split('\n').filter(line => line.trim().length > 20);
|
| 256 |
-
description = lines.length > 0 ? lines[lines.length - 1] : text;
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
return {
|
| 260 |
-
score: Math.min(Math.max(score, 1), 10), // Ensure score is between 1-10
|
| 261 |
-
prediction: prediction.replace(/^["']|["']$/g, ''), // Remove quotes
|
| 262 |
-
description: description.replace(/^["']|["']$/g, '') // Remove quotes
|
| 263 |
-
};
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
async generateImageFromDescription(description) {
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 328 |
-
new SketchToRenderApp();
|
| 329 |
});
|
|
|
|
| 1 |
+
class SketchToRenderApp {
|
| 2 |
+
constructor() {
|
| 3 |
+
this.canvas = document.getElementById('drawingCanvas');
|
| 4 |
+
this.ctx = this.canvas.getContext('2d');
|
| 5 |
+
this.isDrawing = false;
|
| 6 |
+
this.drawingHistory = [];
|
| 7 |
+
this.currentPath = [];
|
| 8 |
+
this.renderedImageUrl = null;
|
| 9 |
+
|
| 10 |
+
// API Key được nhúng trực tiếp trong code
|
| 11 |
+
this.apiKey = 'AIzaSyCinF1_Z7XXPzu_zvMzifKyPIy7i7eUBGc'; // Thay thế bằng API key thực tế
|
| 12 |
+
|
| 13 |
+
this.initializeCanvas();
|
| 14 |
+
this.setupEventListeners();
|
| 15 |
+
this.saveCanvasState();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
initializeCanvas() {
|
| 19 |
+
this.ctx.lineCap = 'round';
|
| 20 |
+
this.ctx.lineJoin = 'round';
|
| 21 |
+
this.ctx.fillStyle = 'white';
|
| 22 |
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
setupEventListeners() {
|
| 26 |
+
// Canvas drawing events
|
| 27 |
+
this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
|
| 28 |
+
this.canvas.addEventListener('mousemove', (e) => this.draw(e));
|
| 29 |
+
this.canvas.addEventListener('mouseup', () => this.stopDrawing());
|
| 30 |
+
this.canvas.addEventListener('mouseout', () => this.stopDrawing());
|
| 31 |
+
|
| 32 |
+
// Touch events for mobile
|
| 33 |
+
this.canvas.addEventListener('touchstart', (e) => {
|
| 34 |
+
e.preventDefault();
|
| 35 |
+
const touch = e.touches[0];
|
| 36 |
+
const mouseEvent = new MouseEvent('mousedown', {
|
| 37 |
+
clientX: touch.clientX,
|
| 38 |
+
clientY: touch.clientY
|
| 39 |
+
});
|
| 40 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
this.canvas.addEventListener('touchmove', (e) => {
|
| 44 |
+
e.preventDefault();
|
| 45 |
+
const touch = e.touches[0];
|
| 46 |
+
const mouseEvent = new MouseEvent('mousemove', {
|
| 47 |
+
clientX: touch.clientX,
|
| 48 |
+
clientY: touch.clientY
|
| 49 |
+
});
|
| 50 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
this.canvas.addEventListener('touchend', (e) => {
|
| 54 |
+
e.preventDefault();
|
| 55 |
+
const mouseEvent = new MouseEvent('mouseup', {});
|
| 56 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Control events
|
| 60 |
+
document.getElementById('brushSize').addEventListener('input', (e) => {
|
| 61 |
+
document.getElementById('brushSizeValue').textContent = e.target.value + 'px';
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
document.getElementById('clearCanvas').addEventListener('click', () => {
|
| 65 |
+
this.clearCanvas();
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
document.getElementById('undoBtn').addEventListener('click', () => {
|
| 69 |
+
this.undo();
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
document.getElementById('generateBtn').addEventListener('click', () => {
|
| 73 |
+
this.generateRender();
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
document.getElementById('downloadBtn').addEventListener('click', () => {
|
| 77 |
+
this.downloadImage();
|
| 78 |
+
});
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
getMousePos(e) {
|
| 82 |
+
const rect = this.canvas.getBoundingClientRect();
|
| 83 |
+
return {
|
| 84 |
+
x: e.clientX - rect.left,
|
| 85 |
+
y: e.clientY - rect.top
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
startDrawing(e) {
|
| 90 |
+
this.isDrawing = true;
|
| 91 |
+
const pos = this.getMousePos(e);
|
| 92 |
+
this.currentPath = [pos];
|
| 93 |
+
|
| 94 |
+
this.ctx.strokeStyle = document.getElementById('brushColor').value;
|
| 95 |
+
this.ctx.lineWidth = document.getElementById('brushSize').value;
|
| 96 |
+
|
| 97 |
+
this.ctx.beginPath();
|
| 98 |
+
this.ctx.moveTo(pos.x, pos.y);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
draw(e) {
|
| 102 |
+
if (!this.isDrawing) return;
|
| 103 |
+
|
| 104 |
+
const pos = this.getMousePos(e);
|
| 105 |
+
this.currentPath.push(pos);
|
| 106 |
+
|
| 107 |
+
this.ctx.lineTo(pos.x, pos.y);
|
| 108 |
+
this.ctx.stroke();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
stopDrawing() {
|
| 112 |
+
if (this.isDrawing) {
|
| 113 |
+
this.isDrawing = false;
|
| 114 |
+
this.saveCanvasState();
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
saveCanvasState() {
|
| 119 |
+
this.drawingHistory.push(this.canvas.toDataURL());
|
| 120 |
+
if (this.drawingHistory.length > 20) {
|
| 121 |
+
this.drawingHistory.shift();
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
clearCanvas() {
|
| 126 |
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 127 |
+
this.ctx.fillStyle = 'white';
|
| 128 |
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 129 |
+
this.saveCanvasState();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
undo() {
|
| 133 |
+
if (this.drawingHistory.length > 1) {
|
| 134 |
+
this.drawingHistory.pop();
|
| 135 |
+
const previousState = this.drawingHistory[this.drawingHistory.length - 1];
|
| 136 |
+
const img = new Image();
|
| 137 |
+
img.onload = () => {
|
| 138 |
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 139 |
+
this.ctx.drawImage(img, 0, 0);
|
| 140 |
+
};
|
| 141 |
+
img.src = previousState;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
async generateRender() {
|
| 146 |
+
if (!this.apiKey || this.apiKey === 'YOUR_GEMINI_API_KEY_HERE') {
|
| 147 |
+
this.showError('Vui lòng cấu hình API Key trong source code');
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Check if canvas is empty
|
| 152 |
+
if (this.isCanvasEmpty()) {
|
| 153 |
+
this.showError('Vui lòng vẽ gì đó trước khi tạo render');
|
| 154 |
+
return;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
this.showLoading(true);
|
| 158 |
+
this.hideError();
|
| 159 |
+
|
| 160 |
+
try {
|
| 161 |
+
// Step 1: Analyze sketch with Gemini
|
| 162 |
+
const analysisResult = await this.analyzeSketchWithGemini();
|
| 163 |
+
console.log('Analysis result:', analysisResult);
|
| 164 |
+
|
| 165 |
+
// Step 2: Generate image using the description from Gemini
|
| 166 |
+
const imageUrl = await this.generateImageFromDescription(analysisResult.description);
|
| 167 |
+
|
| 168 |
+
// Step 3: Display results
|
| 169 |
+
this.displayResults(imageUrl, analysisResult.score, analysisResult.prediction);
|
| 170 |
+
|
| 171 |
+
} catch (error) {
|
| 172 |
+
console.error('Error:', error);
|
| 173 |
+
this.showError(`Lỗi: ${error.message}`);
|
| 174 |
+
} finally {
|
| 175 |
+
this.showLoading(false);
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
isCanvasEmpty() {
|
| 180 |
+
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
| 181 |
+
return imageData.data.every((pixel, index) => {
|
| 182 |
+
if ((index + 1) % 4 === 0) return true; // Alpha channel
|
| 183 |
+
return pixel === 255; // White pixels
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
async analyzeSketchWithGemini() {
|
| 188 |
+
const canvasDataURL = this.canvas.toDataURL('image/png');
|
| 189 |
+
const base64Data = canvasDataURL.split(',')[1];
|
| 190 |
+
|
| 191 |
+
const prompt = `Phân tích bức vẽ sketch này và cung cấp:
|
| 192 |
+
|
| 193 |
+
1. ĐIỂM SỐ: Đánh giá chất lượng và độ rõ ràng từ 1–10, với tiêu chuẩn thoải mái (hình được vẽ bởi người bình thường bằng bút đen trên canvas, không yêu cầu đường nét hoàn hảo).
|
| 194 |
+
2. DỰ ĐOÁN: Tên ngắn gọn của sự vật được vẽ (VD: "Mèo", "Ngôi nhà", "Hoa")
|
| 195 |
+
3. MÔ TẢ: Mô tả ngắn gọn để tạo ảnh render (viết bằng tiếng Anh, phong cách artistic)
|
| 196 |
+
|
| 197 |
+
Định dạng trả lời:
|
| 198 |
+
ĐIỂM: [số]
|
| 199 |
+
DỰ ĐOÁN: [tên sự vật]
|
| 200 |
+
MÔ TẢ: [mô tả ngắn gọn bằng tiếng Anh]`;
|
| 201 |
+
|
| 202 |
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${this.apiKey}`, {
|
| 203 |
+
method: 'POST',
|
| 204 |
+
headers: {
|
| 205 |
+
'Content-Type': 'application/json',
|
| 206 |
+
},
|
| 207 |
+
body: JSON.stringify({
|
| 208 |
+
contents: [{
|
| 209 |
+
parts: [
|
| 210 |
+
{ text: prompt },
|
| 211 |
+
{
|
| 212 |
+
inline_data: {
|
| 213 |
+
mime_type: "image/png",
|
| 214 |
+
data: base64Data
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
]
|
| 218 |
+
}]
|
| 219 |
+
})
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
if (!response.ok) {
|
| 223 |
+
const errorData = await response.json();
|
| 224 |
+
throw new Error(`Gemini API Error: ${errorData.error?.message || 'Unknown error'}`);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
const data = await response.json();
|
| 228 |
+
|
| 229 |
+
if (data.candidates && data.candidates.length > 0) {
|
| 230 |
+
const result = data.candidates[0].content.parts[0].text;
|
| 231 |
+
return this.parseGeminiResponse(result);
|
| 232 |
+
} else {
|
| 233 |
+
throw new Error('Không có kết quả từ Gemini API');
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
parseGeminiResponse(text) {
|
| 238 |
+
console.log('Gemini response:', text);
|
| 239 |
+
|
| 240 |
+
// Extract score
|
| 241 |
+
const scoreMatch = text.match(/ĐIỂM[:\s]*(\d+)/i) || text.match(/score[:\s]*(\d+)/i);
|
| 242 |
+
const score = scoreMatch ? parseInt(scoreMatch[1]) : 7;
|
| 243 |
+
|
| 244 |
+
// Extract prediction
|
| 245 |
+
const predictionMatch = text.match(/DỰ ĐOÁN[:\s]*([^\n]+)/i) || text.match(/prediction[:\s]*([^\n]+)/i);
|
| 246 |
+
const prediction = predictionMatch ? predictionMatch[1].trim() : 'Không xác định';
|
| 247 |
+
|
| 248 |
+
// Extract description
|
| 249 |
+
const descriptionMatch = text.match(/MÔ TẢ[:\s]*([^]*)/i) || text.match(/description[:\s]*([^]*)/i);
|
| 250 |
+
let description = descriptionMatch ? descriptionMatch[1].trim() : text;
|
| 251 |
+
|
| 252 |
+
// If no specific description found, use the whole response
|
| 253 |
+
if (!descriptionMatch) {
|
| 254 |
+
// Try to extract the most descriptive part
|
| 255 |
+
const lines = text.split('\n').filter(line => line.trim().length > 20);
|
| 256 |
+
description = lines.length > 0 ? lines[lines.length - 1] : text;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
return {
|
| 260 |
+
score: Math.min(Math.max(score, 1), 10), // Ensure score is between 1-10
|
| 261 |
+
prediction: prediction.replace(/^["']|["']$/g, ''), // Remove quotes
|
| 262 |
+
description: description.replace(/^["']|["']$/g, '') // Remove quotes
|
| 263 |
+
};
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
async generateImageFromDescription(description) {
|
| 267 |
+
// Tạo prompt kèm style
|
| 268 |
+
const enhancedPrompt = `${description}, high quality, detailed artwork, professional illustration`;
|
| 269 |
+
// Dùng seed ngẫu nhiên (0 để Stability tự lấy seed)
|
| 270 |
+
const seed = Math.floor(Math.random() * 1e6);
|
| 271 |
+
|
| 272 |
+
// Thiết lập endpoint và headers cho Stability AI
|
| 273 |
+
const engineId = 'stable-diffusion-512-v2-1'; // model 512x512
|
| 274 |
+
const url = `https://api.stability.ai/v1/generation/${engineId}/text-to-image`;
|
| 275 |
+
const response = await fetch(url, {
|
| 276 |
+
method: 'POST',
|
| 277 |
+
headers: {
|
| 278 |
+
'Content-Type': 'application/json',
|
| 279 |
+
'Accept': 'application/json',
|
| 280 |
+
'Authorization': `Bearer ${sk-JzOAcde6bqHRjfInzdrNmNjjrCs0LFAvB838xi14OhhEQ06T }`, // API key Stability đã config
|
| 281 |
+
},
|
| 282 |
+
body: JSON.stringify({
|
| 283 |
+
text_prompts: [{ text: enhancedPrompt }],
|
| 284 |
+
cfg_scale: 7,
|
| 285 |
+
width: 512,
|
| 286 |
+
height: 512,
|
| 287 |
+
samples: 1,
|
| 288 |
+
steps: 30,
|
| 289 |
+
seed: seed
|
| 290 |
+
})
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
if (!response.ok) {
|
| 294 |
+
const err = await response.json().catch(() => ({}));
|
| 295 |
+
throw new Error(`Stability API lỗi: ${err.message || response.statusText}`);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
const data = await response.json();
|
| 299 |
+
if (data.artifacts && data.artifacts.length > 0) {
|
| 300 |
+
// Chuyển base64 thành URL hiển thị được
|
| 301 |
+
const base64 = data.artifacts[0].base64;
|
| 302 |
+
return `data:image/png;base64,${base64}`;
|
| 303 |
+
} else {
|
| 304 |
+
throw new Error('Không nhận được ảnh từ Stability AI');
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
showLoading(show) {
|
| 309 |
+
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
| 310 |
+
document.getElementById('resultContent').style.display = show ? 'none' : 'block';
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
showError(message) {
|
| 314 |
+
const errorElement = document.getElementById('errorMessage');
|
| 315 |
+
errorElement.textContent = message;
|
| 316 |
+
errorElement.style.display = 'block';
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
hideError() {
|
| 320 |
+
document.getElementById('errorMessage').style.display = 'none';
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// Initialize the app when the page loads
|
| 325 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 326 |
+
new SketchToRenderApp();
|
|
|
|
|
|
|
| 327 |
});
|