python後端程式碼
from flask import Flask, render_template, request, jsonify, send_from_directory, url_for import os import requests import base64 from PIL import Image import io import random import logging import datetime # 設定日誌記錄 logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') app = Flask(__name__, static_folder='static') app.config['UPLOAD_FOLDER'] = 'static/uploads/' app.config['PROCESSED_FOLDER'] = 'static/processed/' def encode_image_to_base64(image): """Encode image to base64 string for API consumption.""" buffered = io.BytesIO() image.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode('utf-8') def save_decoded_image(b64_image, folder_path, image_name): """Decode base64 image string and save it to specified folder.""" image_data = base64.b64decode(b64_image) seq = 0 output_path = os.path.join(folder_path, f"{image_name}.png") while os.path.exists(output_path): seq += 1 output_path = os.path.join(folder_path, f"{image_name}({seq}).png") with open(output_path, 'wb') as image_file: image_file.write(image_data) return output_path @app.route('/', methods=['GET']) def index(): """Render the main page.""" return render_template('upload.html') @app.route('/upload', methods=['POST']) def upload_file(): """Handle file upload and image processing.""" if 'file' not in request.files: logging.error("No file part in request") return jsonify({'error': 'No file part'}), 400 file = request.files['file'] if file.filename == '': logging.error("No file selected for uploading") return jsonify({'error': 'No selected file'}), 400 if file: filename = file.filename upload_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(upload_path) try: with Image.open(upload_path) as img: encoded_image = encode_image_to_base64(img) # 構建 API 請求資料 data = { "prompt": "Building, pre sketch, masterpiece, best quality, featuring markers", "negative_prompt": "blurry, lower quality, glossy finish,insufficient contrast", "init_images": [encoded_image], "steps": 30, "width": img.width, "height": img.height, "seed": random.randint(1, 10000000), "alwayson_scripts": { "ControlNet": { "args": [ { "enabled": "true", "pixel_perfect": "true", "module": "canny", "model": "control_v11p_sd15_canny_fp16", "weight": 1, "image": encoded_image }, { "enabled": "true", "pixel_perfect": "true", "module": "depth", "model": "control_v11f1p_sd15_depth_fp16", "weight": 1, "image": encoded_image } ] } } } api_url = 'http://127.0.0.1:7860/sdapi/v1/txt2img' # Change to your actual API URL response = requests.post(api_url, json=data) if response.status_code == 200: processed_image_b64 = response.json().get('images')[0] save_path = save_decoded_image(processed_image_b64, app.config['PROCESSED_FOLDER'], "processed_image") timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") return jsonify({'image_url': url_for('get_image', filename=os.path.basename(save_path), ts=timestamp)}) else: logging.error(f"API request failed with status {response.status_code}") return jsonify({'error': 'Failed to get response from API'}), response.status_code except Exception as e: logging.exception("Failed during image processing or API request") return jsonify({'error': str(e)}), 500 @app.route('/images/<filename>') def get_image(filename): """Serve processed image from directory.""" return send_from_directory(app.config['PROCESSED_FOLDER'], filename) if __name__ == '__main__': if not os.path.exists(app.config['UPLOAD_FOLDER']): os.makedirs(app.config['UPLOAD_FOLDER']) if not os.path.exists(app.config['PROCESSED_FOLDER']): os.makedirs(app.config['PROCESSED_FOLDER']) app.run(debug=True, port=5000)
html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>建築效果圖轉彩色手繪</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f2f2f7; color: #1d1d1f; text-align: center; margin: 0; padding: 0; } .container { width: 80%; margin: 40px auto; } .header { margin-bottom: 20px; } .header h1 { font-size: 28px; font-weight: normal; } .header p { font-size: 18px; color: #6e6e73; } .content { display: flex; justify-content: space-around; margin-top: 20px; } .upload-section, .result-section { flex: 1; background: #ffffff; border-radius: 12px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); padding: 20px; margin: 0 10px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; } img { max-width: 100%; height: auto; border-radius: 8px; margin-bottom: 20px; } button { padding: 10px 20px; font-size: 16px; border: none; border-radius: 20px; cursor: pointer; transition: background-color 0.3s ease; width: 100%; margin-bottom: 10px; } .red-button { background-color: #ff0000; color: #ffffff; } .red-button:hover { background-color: #cc0000; } .disabled { background-color: #c0c0c0; color: #6e6e73; cursor: not-allowed; } #file-input { display: none; } </style> </head> <body> <div class="container"> <div class="header"> <h1>建築效果圖轉彩色手繪</h1> <p>使用介紹: 請在左側框內上傳一張建築渲染圖</p> </div> <div class="content"> <div class="upload-section"> <input type="file" id="file-input" hidden /> <label for="file-input" class="upload-button"> <img id="original-image" src="/static/AI建築原圖.png" alt="AI建築原圖" /> <button id="upload-btn">上傳參考圖</button> </label> <button id="call-api-btn" class="red-button disabled" disabled>開始生成</button> </div> <div class="result-section"> <img id="sketched-image" src="/static/AI建築手繪.png" alt="AI建築手繪圖" /> </div> </div> </div> <script> const fileInput = document.getElementById('file-input'); const originalImage = document.getElementById('original-image'); const sketchedImage = document.getElementById('sketched-image'); const uploadButton = document.getElementById('upload-btn'); const callApiButton = document.getElementById('call-api-btn'); fileInput.addEventListener('change', function(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { originalImage.src = e.target.result; callApiButton.classList.remove('disabled'); callApiButton.disabled = false; }; reader.readAsDataURL(file); } }); uploadButton.addEventListener('click', function() { fileInput.click(); }); callApiButton.addEventListener('click', async function() { this.classList.add('disabled'); this.disabled = true; const formData = new FormData(); formData.append('file', fileInput.files[0]); try { const response = await fetch('/upload', { method: 'POST', body: formData }); const data = await response.json(); if (data.image_url) { const newSrc = data.image_url + "&ts=" + new Date().getTime(); // 新增時間戳以避免瀏覽器快取問題 sketchedImage.src = newSrc; } else { alert(data.error || '上傳或處理失敗'); } } catch (error) { console.error('Error:', error); alert('上傳或處理失敗'); } finally { this.classList.remove('disabled'); this.disabled = false; } }); callApiButton.classList.add('disabled'); callApiButton.disabled = true; // 初始時禁用 </script> </body> </html>