index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI服務平臺</title> <link rel="stylesheet" href="static/css/styles.css"> <script src="static/js/script.js"></script> </head> <body> <h1 class="main-title">AI應用賦能設計行業</h1> <h2 class="sub-title">作者:zly</h2> <!-- 工業設計分類 --> <h3 class="section-title">工業設計:用AI開一家電商店</h3> <div class="grid-container"> <a href="/detail_1" target="_self" class="card"> <img class="card-image" src="static/images/1.png" alt="AI建築轉馬克筆手繪"> <div class="card-content"> <h3>AI建築轉馬克筆手繪</h3> <p>為成品建築方案新增一個手繪稿</p> </div> </a> <a href="/detail_2" target="_blank" class="card"> <img class="card-image" src="static/images/2.png" alt="AI視覺創作"> <div class="card-content"> <h3>AI視覺創作</h3> <p>一鍵生成獨特的視覺作品和元素</p> </div> </a> <a href="/detail_3" target="_blank" class="card"> <img class="card-image" src="static/images/3.png" alt="AI視覺創作"> <div class="card-content"> <h3>AI視覺創作</h3> <p>一鍵生成獨特的視覺作品和元素</p> </div> </a> <a href="/detail_4" target="_blank" class="card"> <img class="card-image" src="static/images/4.png" alt="AI視覺創作"> <div class="card-content"> <h3>AI視覺創作</h3> <p>一鍵生成獨特的視覺作品和元素</p> </div> </a> </div> <!-- 室內設計分類、智慧文生圖應用分類,以及其他卡片和類別可按此格式繼續新增 --> <script src="static/js/script.js"></script> </body> </html>
style.css
/* 重置部分預設樣式 */ body, html { margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f4f4; } /* 主標題樣式 */ .main-title { text-align: center; font-size: 28px; font-weight: bold; color: black; margin-top: 30px; margin-bottom: 10px; } /* 副標題樣式 */ .sub-title { text-align: center; font-size: 15px; color: gray; margin-bottom: 20px; font-weight: normal; /* 確保副標題不加粗 */ } /* 小標題樣式 */ .section-title { font-size: 24px; color: black; margin: 40px 0 20px; text-align: center; } /* 網格佈局容器 */ .grid-container { display: grid; grid-template-columns: repeat(4, 1fr); /* 四列布局 */ gap: 20px; /* 網格間距 */ padding: 20px; /* 容器內邊距 */ max-width: 1200px; margin: 0 auto; /* 水平居中 */ } /* 卡片樣式 */ .card { background-color: #ffffff; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); overflow: hidden; cursor: pointer; transition: transform 0.2s ease-in-out; display: flex; flex-direction: column; align-items: center; text-decoration: none; color: inherit; } .card:hover { transform: scale(1.05); } /* 卡片圖片樣式 */ .card-image { width: 100%; height: auto; max-height: 150px; object-fit: cover; border-top-left-radius: 10px; border-top-right-radius: 10px; } /* 卡片內容樣式 */ .card-content { padding: 15px; text-align: center; flex: 1; } .card h3, .card p { margin: 10px 0; } /* 響應式佈局調整 */ @media (max-width: 1024px) { .grid-container { grid-template-columns: repeat(2, 1fr); /* 在較小螢幕上使用兩列布局 */ } } @media (max-width: 768px) { .grid-container { grid-template-columns: 1fr; /* 在最小螢幕上使用單列布局 */ padding: 10px; } } /* 小標題樣式 */ .section-title { font-size: 20px; /* 字型大小調整 */ font-weight: normal; /* 取消加粗 */ color: black; margin: 40px 0 20px; text-align: center; padding-bottom: 5px; /* 底部填充,為底框留出空間 */ border-bottom: 2px solid #007BFF; /* 新增藍色的底框 */ width: fit-content; /* 使寬度適應內容 */ margin-left: auto; /* 水平居中 */ margin-right: auto; }
app.py
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 import pandas as pd # 設定日誌記錄 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 load_details(): try: details_df = pd.read_excel('static/excel/介面切換.xlsx', sheet_name='Sheet1', index_col=0) # 將DataFrame轉置以便每一列變成一行,這樣列名(即HTML頁面名稱)成為字典的鍵 details_df = details_df.transpose() details = {} for detail_id, row in details_df.iterrows(): details[detail_id] = { "prompt": row['prompt'], "negative_prompt": row['negative_prompt'], "left_image": row['左框圖片'], "right_image": row['右框圖片'] } return details except Exception as e: print("讀取詳情時出錯:", e) return {} details = load_details() 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('/') def home(): return render_template('index.html') @app.route('/detail_<int:detail_id>', methods=['GET', 'POST']) def detail(detail_id): if request.method == 'GET': # 從details字典中獲取對應的引數 detail_info = details.get(f'detail_{detail_id}', {}) return render_template(f'detail_{detail_id}.html', detail=detail_info) @app.route('/upload/<detail_id>', methods=['POST']) def upload_file(detail_id): """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 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) detail_info = details.get(detail_id, {}) data = { "prompt": detail_info.get('prompt', 'Default prompt'), "negative_prompt": detail_info.get('negative_prompt', 'Default negative prompt'), "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'], filename) timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") image_url = url_for('get_image', filename=os.path.basename(save_path), _external=True) + f"?ts={timestamp}" logging.debug(f"Generated image URL: {image_url}") # 日誌記錄生成的URL return jsonify({'image_url': image_url}) 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']) # 設定 host='0.0.0.0' 允許從任何 IP 訪問 app.run(host='0.0.0.0', port=5000, debug=True) # 將 debug 設定為 False 在生產環境中
scripts.js
// 當卡片被點選時呼叫此函式 function onCardClick(cardIndex) { alert('卡片 ' + cardIndex + ' 被點選'); }
detail_1
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>建築效果圖轉彩色手繪 - 詳情1</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; } .loader-container { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; } .loader { border: 6px solid #eeeeee; /* Light grey */ border-top: 6px solid #3498db; /* Blue */ border-radius: 50%; width: 40px; height: 40px; animation: spin 2s linear infinite; } .loading-text { margin-top: 10px; font-size: 16px; color: #3498db; /* Blue */ font-weight: bold; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </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="{{ detail.left_image }}" 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="{{ detail.right_image }}" alt="AI建築手繪圖" /> <!-- 新增下載按鈕 --> <a href="{{ detail.right_image }}" download="ArchitecturalSketch.png"> <button>下載圖片</button> </a> </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'); const resultSection = document.querySelector('.result-section'); 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; resultSection.innerHTML = ` <div class="loader-container"> <div class="loader"></div> <div class="loading-text">生成中...</div> </div>`; const formData = new FormData(); formData.append('file', fileInput.files[0]); try { const response = await fetch('/upload/detail_1', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { if (data.image_url) { let imageUrl = new URL(data.image_url); imageUrl.searchParams.set('t', new Date().getTime().toString()); sketchedImage.src = imageUrl.href; resultSection.innerHTML = `<img src="${imageUrl.href}" alt="AI建築手繪圖" /> <a href="${imageUrl.href}" download="ArchitecturalSketch.png"> <button>下載圖片</button> </a>`; } else { throw new Error('Image URL is missing from the response'); } } else { throw new Error(data.error || '上傳失敗'); } } catch (error) { console.error('上傳或處理失敗', error); alert('上傳或處理失敗: ' + error.message); resultSection.innerHTML = ''; } finally { this.classList.remove('disabled'); this.disabled = false; } }); </script> </body> </html>