| <!DOCTYPE html> |
| <html lang="zh-CN"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>M5Stack LLM8850 文生图展示</title> |
| <style> |
| :root { |
| --primary-color: #3498db; |
| --selected-color: #e74c3c; |
| --success-color: #27ae60; |
| --warning-color: #f39c12; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| margin: 0; |
| padding: 20px; |
| background: #f0f3f5; |
| } |
| |
| .container { |
| max-width: 100%; |
| margin: 0 auto; |
| background: white; |
| border-radius: 12px; |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); |
| padding: 2rem; |
| position: relative; |
| } |
| |
| |
| .company-logo { |
| position: absolute; |
| top: 20px; |
| right: 20px; |
| width: 80px; |
| height: auto; |
| } |
| |
| h1 { |
| text-align: center; |
| color: #2c3e50; |
| margin-bottom: 2rem; |
| font-weight: 600; |
| margin-right: 100px; |
| |
| } |
| |
| |
| #selected-terms-box { |
| position: sticky; |
| top: 0; |
| background: rgba(255, 255, 255, 0.95); |
| backdrop-filter: blur(5px); |
| z-index: 10; |
| padding: 1rem; |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); |
| margin-bottom: 1.5rem; |
| border-radius: 8px; |
| } |
| |
| .selected-terms-label { |
| font-weight: 500; |
| color: #2c3e50; |
| margin-bottom: 0.5rem; |
| font-size: 0.9rem; |
| } |
| |
| .selected-term { |
| display: inline-flex; |
| align-items: center; |
| padding: 0.4rem 0.8rem; |
| background: var(--selected-color); |
| color: white; |
| border-radius: 20px; |
| margin: 0.3rem; |
| font-size: 0.9em; |
| animation: popIn 0.2s ease; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| } |
| |
| .selected-term:hover { |
| background: #c0392b; |
| transform: scale(1.05); |
| } |
| |
| .controls { |
| display: inline-flex; |
| gap: 0.5rem; |
| margin-left: 1rem; |
| vertical-align: middle; |
| } |
| |
| .btn { |
| padding: 0.6rem 1.2rem; |
| border: none; |
| border-radius: 20px; |
| font-size: 0.9rem; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| font-weight: 500; |
| } |
| |
| #generate-btn { |
| background: var(--primary-color); |
| color: white; |
| padding: 1rem 2rem; |
| |
| font-size: 1.1rem; |
| font-weight: 600; |
| } |
| |
| #random-btn { |
| background: var(--success-color); |
| color: white; |
| } |
| |
| #clear-terms-btn { |
| background: var(--warning-color); |
| color: white; |
| } |
| |
| #clear-images-btn { |
| background: #95a5a6; |
| color: white; |
| } |
| |
| .btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); |
| } |
| |
| .btn:disabled { |
| background: #bdc3c7; |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: none; |
| } |
| |
| @keyframes popIn { |
| from { |
| transform: scale(0.8); |
| opacity: 0; |
| } |
| |
| to { |
| transform: scale(1); |
| opacity: 1; |
| } |
| } |
| |
| |
| .progress-container { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| padding: 1rem; |
| border-radius: 8px; |
| margin-bottom: 1rem; |
| color: white; |
| display: none; |
| animation: slideDown 0.3s ease; |
| } |
| |
| .progress-container.active { |
| display: block; |
| } |
| |
| .progress-bar { |
| width: 100%; |
| height: 6px; |
| background: rgba(255, 255, 255, 0.3); |
| border-radius: 3px; |
| overflow: hidden; |
| margin: 0.5rem 0; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: linear-gradient(90deg, #fff, #f39c12); |
| width: 0%; |
| transition: width 0.3s ease; |
| border-radius: 3px; |
| } |
| |
| .progress-info { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| font-size: 0.9rem; |
| } |
| |
| .progress-step { |
| opacity: 0.9; |
| } |
| |
| .fun-fact { |
| background: rgba(255, 255, 255, 0.1); |
| padding: 0.5rem; |
| border-radius: 4px; |
| margin-top: 0.5rem; |
| font-style: italic; |
| font-size: 0.85rem; |
| animation: fadeIn 0.5s ease; |
| } |
| |
| @keyframes slideDown { |
| from { |
| opacity: 0; |
| transform: translateY(-20px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| |
| #term-categories { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| gap: 1.5rem; |
| } |
| |
| .category { |
| background: #ffffff; |
| border-radius: 8px; |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| overflow: hidden; |
| } |
| |
| .category-header { |
| padding: 1rem; |
| background: var(--primary-color); |
| color: white; |
| font-weight: 500; |
| position: sticky; |
| top: 0; |
| z-index: 1; |
| } |
| |
| .scroll-container { |
| max-height: 240px; |
| overflow-y: hidden; |
| padding: 1rem; |
| position: relative; |
| } |
| |
| .term-item { |
| padding: 0.8rem 1.2rem; |
| margin: 0.5rem 0; |
| border-radius: 6px; |
| background: #f8f9fa; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| position: relative; |
| overflow: hidden; |
| display: flex; |
| align-items: center; |
| min-height: 48px; |
| } |
| |
| .term-content { |
| display: flex; |
| flex-direction: column; |
| gap: 0.2rem; |
| } |
| |
| .chinese { |
| font-weight: 500; |
| color: #2c3e50; |
| } |
| |
| .term { |
| font-size: 0.85rem; |
| color: #7f8c8d; |
| font-style: italic; |
| } |
| |
| .term-item.selected { |
| background: var(--primary-color); |
| color: white; |
| } |
| |
| .term-item.selected .term { |
| color: rgba(255, 255, 255, 0.8); |
| } |
| |
| .term-item:hover { |
| transform: translateX(5px); |
| box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1); |
| } |
| |
| |
| .main-content { |
| display: grid; |
| grid-template-columns: 1fr 500px; |
| gap: 2rem; |
| min-height: 70vh; |
| } |
| |
| .left-column { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); |
| gap: 1.2rem; |
| align-content: start; |
| } |
| |
| .right-column { |
| background: #f8f9fa; |
| border-radius: 8px; |
| padding: 1rem; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .images-header { |
| text-align: center; |
| color: #2c3e50; |
| margin-bottom: 1rem; |
| font-size: 1.1rem; |
| font-weight: 500; |
| flex-shrink: 0; |
| } |
| |
| |
| #images-container { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 1rem; |
| flex: 1; |
| } |
| |
| .image-item { |
| background: white; |
| border-radius: 8px; |
| padding: 0.5rem; |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| animation: zoomIn 0.3s ease; |
| align-self: start; |
| } |
| |
| .image-item img { |
| width: 100%; |
| height: auto; |
| border-radius: 6px; |
| display: block; |
| } |
| |
| .image-terms { |
| font-size: 0.75rem; |
| color: #7f8c8d; |
| margin-top: 0.5rem; |
| padding: 0.5rem; |
| background: #ecf0f1; |
| border-radius: 4px; |
| word-wrap: break-word; |
| line-height: 1.3; |
| } |
| |
| .empty-state { |
| grid-column: 1 / -1; |
| text-align: center; |
| color: #7f8c8d; |
| padding: 2rem; |
| font-style: italic; |
| } |
| |
| @keyframes zoomIn { |
| from { |
| opacity: 0; |
| transform: scale(0.9); |
| } |
| |
| to { |
| opacity: 1; |
| transform: scale(1); |
| } |
| } |
| |
| |
| @media (max-width: 1400px) { |
| .main-content { |
| grid-template-columns: 1fr 450px; |
| } |
| } |
| |
| @media (max-width: 1200px) { |
| .main-content { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .container { |
| padding: 1rem; |
| } |
| |
| .company-logo { |
| width: 60px; |
| top: 10px; |
| right: 10px; |
| } |
| |
| h1 { |
| margin-right: 70px; |
| font-size: 1.5rem; |
| } |
| |
| #term-categories { |
| grid-template-columns: 1fr; |
| } |
| |
| #images-container { |
| grid-template-columns: 1fr; |
| } |
| |
| .controls { |
| flex-direction: column; |
| gap: 0.5rem; |
| margin-left: 0; |
| margin-top: 1rem; |
| } |
| |
| .btn { |
| padding: 0.8rem 1.5rem; |
| } |
| |
| #generate-btn { |
| padding: 1rem 1.5rem; |
| } |
| } |
| |
| |
| @media (min-width: 1600px) { |
| .container { |
| max-width: 95%; |
| } |
| |
| .main-content { |
| grid-template-columns: 1fr 600px; |
| } |
| |
| .left-column { |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="container"> |
| |
| <img src="https://docs.m5stack.com/assets/m5logo2022.svg" alt="M5Stack Logo" class="company-logo"> |
|
|
| <h1>M5Stack AI Pyrmind AX8850 文生图展示</h1> |
|
|
| <div id="selected-terms-box"> |
| <div class="selected-terms-label">已选择的提示词 Selected Prompts:</div> |
| <div id="selected-terms-list"></div> |
| <div class="controls"> |
| <button id="generate-btn" class="btn">立即生成 Generate Now</button> |
| <button id="random-btn" class="btn">随机描述 Random Terms</button> |
| <button id="clear-terms-btn" class="btn">清除提示词 Clear Terms</button> |
| <button id="clear-images-btn" class="btn">清除图片 Clear Images</button> |
| </div> |
| </div> |
|
|
| |
| <div class="progress-container" id="progress-container"> |
| <div class="progress-info"> |
| <span>AI 正在创作中... AI is creating...</span> |
| <span id="progress-percent">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="progress-fill"></div> |
| </div> |
| <div class="progress-step" id="progress-step">初始化中... Initializing...</div> |
| <div class="fun-fact" id="fun-fact"></div> |
| </div> |
|
|
| <div class="main-content"> |
| <div class="left-column"> |
| <div id="term-categories"></div> |
| </div> |
|
|
| <div class="right-column"> |
| <div class="images-header">生成历史 Generation History</div> |
| <div id="images-container"> |
| <div class="empty-state"> |
| 选择词条并点击生成按钮开始创作<br> |
| Select terms and click generate to start creating |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| let scrollControllers = new Map(); |
| |
| const funFacts = [ |
| "正在分析你的创意想法... Analyzing your creative ideas...", |
| "AI 大脑正在思考最佳构图... AI brain thinking about optimal composition...", |
| "调色板准备中,寻找完美色彩... Preparing palette, finding perfect colors...", |
| "像素精灵们正在排队组合... Pixel sprites lining up for combination...", |
| "魔法正在将文字转化为视觉... Magic transforming text to visuals...", |
| "创意工厂全速运转中... Creative factory at full speed...", |
| "每个细节都在精心雕琢... Every detail carefully crafted...", |
| "光影大师正在调整明暗... Light master adjusting brightness...", |
| "笔刷正在画布上飞舞... Brushes dancing on canvas...", |
| "艺术灵感正在汇聚成形... Artistic inspiration taking shape...", |
| "创造力引擎火力全开... Creativity engine at full power...", |
| "美学算法正在优化细节... Aesthetic algorithm optimizing details...", |
| "视觉交响乐即将奏响... Visual symphony about to play...", |
| "想象力正在突破边界... Imagination breaking boundaries...", |
| "奇迹工坊正在施展魔法... Miracle workshop casting magic..." |
| ]; |
| |
| |
| const unetSteps = [ |
| "预处理输入图层... Preprocessing input layers...", |
| "第1层卷积神经网络处理... Layer 1 CNN processing...", |
| "第2层特征提取中... Layer 2 feature extraction...", |
| "第3层语义理解... Layer 3 semantic understanding...", |
| "第4层细节优化... Layer 4 detail optimization...", |
| "第5层色彩调和... Layer 5 color harmony...", |
| "第6层纹理生成... Layer 6 texture generation...", |
| "第7层构图调整... Layer 7 composition adjustment...", |
| "第8层光影处理... Layer 8 lighting processing...", |
| "第9层细节增强... Layer 9 detail enhancement...", |
| "第10层质量检查... Layer 10 quality check...", |
| "第11层风格融合... Layer 11 style fusion...", |
| "第12层最终调色... Layer 12 final coloring...", |
| "第13层锐化处理... Layer 13 sharpening...", |
| "第14层噪声去除... Layer 14 noise removal...", |
| "第15层对比度优化... Layer 15 contrast optimization...", |
| "第16层饱和度调节... Layer 16 saturation adjustment...", |
| "第17层最终渲染... Layer 17 final rendering...", |
| "第18层质量验证... Layer 18 quality validation...", |
| "第19层输出准备... Layer 19 output preparation...", |
| "第20层完成处理... Layer 20 completion..." |
| ]; |
| |
| async function loadAndDisplayTerms() { |
| try { |
| const response = await fetch('./person.json'); |
| const data = await response.json(); |
| const proprietaryTerms = data.prompt_terms.filter(term => term.type === "专用"); |
| const categories = proprietaryTerms.reduce((acc, term) => { |
| const subtype = term.subtype || "通用 General"; |
| acc[subtype] = acc[subtype] || []; |
| acc[subtype].push(term); |
| return acc; |
| }, {}); |
| const container = document.getElementById('term-categories'); |
| Object.entries(categories).forEach(([subtype, terms]) => { |
| const category = document.createElement('div'); |
| category.className = 'category'; |
| const header = document.createElement('div'); |
| header.className = 'category-header'; |
| header.textContent = subtype; |
| const scrollContainer = document.createElement('div'); |
| scrollContainer.className = 'scroll-container'; |
| terms.forEach(term => { |
| const item = document.createElement('div'); |
| item.className = 'term-item'; |
| item.dataset.term = term.term; |
| item.innerHTML = ` |
| <div class="term-content"> |
| <div class="chinese">${term.chinese}</div> |
| <div class="term">${term.term}</div> |
| </div> |
| `; |
| scrollContainer.appendChild(item); |
| }); |
| category.append(header, scrollContainer); |
| container.appendChild(category); |
| if (scrollContainer.scrollHeight > scrollContainer.clientHeight) { |
| setupAutoScroll(scrollContainer); |
| } |
| }); |
| } catch (error) { |
| console.error('数据加载失败 Data loading failed:', error); |
| } |
| } |
| |
| function setupAutoScroll(container) { |
| let isPaused = false; |
| let scrollPos = 0; |
| const scrollSpeed = 0.5; |
| const scrollHeight = container.scrollHeight - container.clientHeight; |
| container.addEventListener('mouseenter', () => isPaused = true); |
| container.addEventListener('mouseleave', () => isPaused = false); |
| const scroll = () => { |
| if (!isPaused && scrollHeight > 0) { |
| scrollPos = (scrollPos + scrollSpeed) % scrollHeight; |
| const easedPos = scrollPos < scrollHeight / 2 ? scrollPos : scrollHeight - scrollPos; |
| container.scrollTop = easedPos; |
| } |
| requestAnimationFrame(scroll); |
| }; |
| scroll(); |
| } |
| |
| function updateSelections() { |
| const selectedItems = Array.from(document.querySelectorAll('.term-item.selected')); |
| const container = document.getElementById('selected-terms-list'); |
| |
| container.innerHTML = selectedItems.map(item => { |
| const chinese = item.querySelector('.chinese').textContent; |
| const english = item.querySelector('.term').textContent; |
| return `<span class="selected-term" data-term="${english}">${chinese} ${english}</span>`; |
| }).join(''); |
| } |
| |
| function showProgress() { |
| const container = document.getElementById('progress-container'); |
| const progressFill = document.getElementById('progress-fill'); |
| const progressStep = document.getElementById('progress-step'); |
| const progressPercent = document.getElementById('progress-percent'); |
| const funFact = document.getElementById('fun-fact'); |
| container.classList.add('active'); |
| let currentStep = 0; |
| const totalSteps = 20; |
| const stepDuration = 240; |
| return new Promise((resolve) => { |
| const interval = setInterval(() => { |
| currentStep++; |
| const progress = (currentStep / totalSteps) * 100; |
| progressFill.style.width = progress + '%'; |
| progressPercent.textContent = Math.round(progress) + '%'; |
| progressStep.textContent = unetSteps[currentStep - 1] || '处理中... Processing...'; |
| if (currentStep % 3 === 0) { |
| const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; |
| funFact.textContent = randomFact; |
| } |
| if (currentStep >= totalSteps) { |
| clearInterval(interval); |
| progressStep.textContent = '即将完成... Almost done...'; |
| setTimeout(() => { |
| container.classList.remove('active'); |
| resolve(); |
| }, 500); |
| } |
| }, stepDuration); |
| }); |
| } |
| |
| function addImageToHistory(selectedTerms) { |
| const imagesContainer = document.getElementById('images-container'); |
| |
| const emptyState = imagesContainer.querySelector('.empty-state'); |
| if (emptyState) { |
| emptyState.remove(); |
| } |
| const imgContainer = document.createElement('div'); |
| imgContainer.className = 'image-item'; |
| const img = document.createElement('img'); |
| img.src = 'generated_image.png?' + new Date().getTime(); |
| img.alt = 'Generated Image'; |
| const termsDiv = document.createElement('div'); |
| termsDiv.className = 'image-terms'; |
| termsDiv.textContent = '关键词 Keywords: ' + selectedTerms.join(', '); |
| imgContainer.appendChild(img); |
| imgContainer.appendChild(termsDiv); |
| |
| imagesContainer.prepend(imgContainer); |
| |
| while (imagesContainer.children.length > 20) { |
| imagesContainer.removeChild(imagesContainer.lastChild); |
| } |
| } |
| |
| function clearAllTerms() { |
| |
| document.querySelectorAll('.term-item.selected').forEach(item => { |
| item.classList.remove('selected'); |
| }); |
| updateSelections(); |
| } |
| |
| function clearAllImages() { |
| const imagesContainer = document.getElementById('images-container'); |
| imagesContainer.innerHTML = '<div class="empty-state">选择词条并点击生成按钮开始创作<br>Select terms and click generate to start creating</div>'; |
| } |
| |
| |
| function selectRandomTerms() { |
| |
| clearAllTerms(); |
| |
| |
| const categories = document.querySelectorAll('.category'); |
| const randomTerms = []; |
| |
| categories.forEach(category => { |
| const termItems = category.querySelectorAll('.term-item'); |
| if (termItems.length > 0) { |
| |
| const randomIndex = Math.floor(Math.random() * termItems.length); |
| const selectedItem = termItems[randomIndex]; |
| selectedItem.classList.add('selected'); |
| randomTerms.push({ |
| chinese: selectedItem.querySelector('.chinese').textContent, |
| english: selectedItem.querySelector('.term').textContent |
| }); |
| } |
| }); |
| |
| |
| updateSelections(); |
| |
| |
| if (randomTerms.length > 0) { |
| console.log('随机选择的词条:', randomTerms); |
| |
| const notification = document.createElement('div'); |
| notification.style.cssText = ` |
| position: fixed; |
| top: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: var(--success-color); |
| color: white; |
| padding: 0.8rem 1.5rem; |
| border-radius: 20px; |
| z-index: 1000; |
| animation: slideDown 0.3s ease; |
| `; |
| notification.textContent = `已随机选择 ${randomTerms.length} 个词条 Randomly selected ${randomTerms.length} terms`; |
| document.body.appendChild(notification); |
| |
| |
| setTimeout(() => { |
| notification.remove(); |
| }, 3000); |
| } |
| } |
| |
| |
| document.addEventListener('click', (e) => { |
| |
| if (e.target.closest('.term-item')) { |
| const item = e.target.closest('.term-item'); |
| item.classList.toggle('selected'); |
| updateSelections(); |
| } |
| |
| if (e.target.closest('.selected-term')) { |
| const term = e.target.closest('.selected-term').dataset.term; |
| const termItem = document.querySelector(`.term-item[data-term="${term}"]`); |
| if (termItem) { |
| termItem.classList.remove('selected'); |
| updateSelections(); |
| } |
| } |
| }); |
| |
| window.addEventListener('load', () => { |
| loadAndDisplayTerms(); |
| const generateBtn = document.getElementById('generate-btn'); |
| const randomBtn = document.getElementById('random-btn'); |
| const clearTermsBtn = document.getElementById('clear-terms-btn'); |
| const clearImagesBtn = document.getElementById('clear-images-btn'); |
| |
| |
| generateBtn.addEventListener('click', async () => { |
| const selectedTerms = Array.from(document.querySelectorAll('.term-item.selected')) |
| .map(item => item.dataset.term); |
| if (selectedTerms.length === 0) { |
| alert('请至少选择一个词条 Please select at least one term'); |
| return; |
| } |
| generateBtn.disabled = true; |
| const progressPromise = showProgress(); |
| try { |
| const [response] = await Promise.all([ |
| fetch('/generate', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ terms: selectedTerms }) |
| }), |
| progressPromise |
| ]); |
| if (response.ok) { |
| addImageToHistory(selectedTerms); |
| } else { |
| const error = await response.json(); |
| alert(`生成失败 Generation failed: ${error.error}`); |
| } |
| } catch (error) { |
| console.error('生成请求失败 Generation request failed:', error); |
| alert('生成请求失败,请检查网络连接 Generation request failed, please check network connection'); |
| } finally { |
| generateBtn.disabled = false; |
| } |
| }); |
| randomBtn.addEventListener('click', () => { |
| selectRandomTerms(); |
| }); |
| |
| |
| clearTermsBtn.addEventListener('click', clearAllTerms); |
| |
| |
| clearImagesBtn.addEventListener('click', () => { |
| if (confirm('确定要清除所有生成的图片吗? Are you sure you want to clear all generated images?')) { |
| clearAllImages(); |
| } |
| }); |
| }); |
| </script> |
| </body> |
|
|
| </html> |
|
|