docs: update content and components across multiple files
- Refine chapter introductions in zh-cn docs for clarity and conciseness - Update navigation links to include '/easy-vibe' prefix - Simplify UI components (ChapterIntroduction, ContextWindowVisualizer) - Add new agent-related demo components (AgentMemoryDemo, AgentToolUseDemo) - Improve context compression demo with better visuals and metrics - Adjust styling and layout across various components
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
<!--
|
||||
UNetDenoiseDemo.vue
|
||||
UNet 去噪过程演示组件
|
||||
|
||||
用途:
|
||||
展示 UNet/DiT 如何从噪声中逐步恢复图像,理解扩散模型的核心去噪机制。
|
||||
|
||||
交互功能:
|
||||
- 单步/自动播放去噪过程
|
||||
- 可视化噪声预测
|
||||
- 展示不同时间步的预测结果
|
||||
- 对比有/无文本引导的生成
|
||||
-->
|
||||
<template>
|
||||
<div class="unet-demo">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="header-controls">
|
||||
<span class="title">🧠 UNet 去噪模型</span>
|
||||
<div class="controls">
|
||||
<el-button-group>
|
||||
<el-button @click="stepBackward" :disabled="currentStep <= 0">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
</el-button>
|
||||
<el-button @click="togglePlay">
|
||||
<el-icon v-if="isPlaying"><VideoPause /></el-icon>
|
||||
<el-icon v-else><VideoPlay /></el-icon>
|
||||
</el-button>
|
||||
<el-button @click="stepForward" :disabled="currentStep >= totalSteps">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 主展示区 -->
|
||||
<div class="main-display">
|
||||
<div class="display-section">
|
||||
<div class="section-label">当前噪声图像 (Noisy Image)</div>
|
||||
<canvas
|
||||
ref="noisyCanvas"
|
||||
width="256"
|
||||
height="256"
|
||||
class="display-canvas"
|
||||
/>
|
||||
<div class="timestep-info">
|
||||
<el-tag type="info">Timestep: {{ currentStep }} / {{ totalSteps }}</el-tag>
|
||||
<el-tag :type="getNoiseLevelType()">噪声强度: {{ getNoiseLevel() }}%</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-section">
|
||||
<el-icon :size="24"><ArrowRight /></el-icon>
|
||||
<div class="model-box">
|
||||
<div class="model-name">UNet / DiT</div>
|
||||
<div class="model-desc">预测噪声</div>
|
||||
</div>
|
||||
<el-icon :size="24"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="display-section">
|
||||
<div class="section-label">预测的噪声 (Predicted Noise)</div>
|
||||
<canvas
|
||||
ref="noiseCanvas"
|
||||
width="256"
|
||||
height="256"
|
||||
class="display-canvas noise-preview"
|
||||
/>
|
||||
<div class="noise-stats">
|
||||
<el-tag size="small" type="warning">噪声估计</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-section">
|
||||
<el-icon :size="24"><ArrowRight /></el-icon>
|
||||
<div class="operation-box">
|
||||
<div class="op-name">减法</div>
|
||||
<div class="op-formula">x - ε</div>
|
||||
</div>
|
||||
<el-icon :size="24"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
|
||||
<div class="display-section">
|
||||
<div class="section-label">去噪结果 (Denoised)</div>
|
||||
<canvas
|
||||
ref="denoisedCanvas"
|
||||
width="256"
|
||||
height="256"
|
||||
class="display-canvas"
|
||||
/>
|
||||
<div class="progress-info">
|
||||
<el-progress
|
||||
:percentage="(currentStep / totalSteps) * 100"
|
||||
:status="currentStep === totalSteps ? 'success' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间轴 -->
|
||||
<div class="timeline-section">
|
||||
<div class="timeline-label">去噪时间轴</div>
|
||||
<el-slider
|
||||
v-model="currentStep"
|
||||
:min="0"
|
||||
:max="totalSteps"
|
||||
:step="1"
|
||||
show-stops
|
||||
:marks="marks"
|
||||
@input="updateDisplay"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 对比模式 -->
|
||||
<div class="compare-section">
|
||||
<el-switch
|
||||
v-model="showComparison"
|
||||
active-text="显示对比 (有/无文本引导)"
|
||||
/>
|
||||
<div v-if="showComparison" class="compare-display">
|
||||
<div class="compare-item">
|
||||
<div class="compare-label">无引导 (Unconditional)</div>
|
||||
<canvas
|
||||
ref="uncondCanvas"
|
||||
width="200"
|
||||
height="200"
|
||||
class="compare-canvas"
|
||||
/>
|
||||
</div>
|
||||
<div class="compare-item">
|
||||
<div class="compare-label">有引导 (CFG Scale=7.5)</div>
|
||||
<canvas
|
||||
ref="condCanvas"
|
||||
width="200"
|
||||
height="200"
|
||||
class="compare-canvas"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>去噪原理:</strong>
|
||||
UNet 学习预测图像中的噪声,然后用原图减去预测的噪声,得到更清晰的结果。重复这个过程,直到从纯噪声恢复出清晰图像。
|
||||
</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ArrowRight, ArrowLeft, VideoPlay, VideoPause } from '@element-plus/icons-vue'
|
||||
|
||||
const noisyCanvas = ref(null)
|
||||
const noiseCanvas = ref(null)
|
||||
const denoisedCanvas = ref(null)
|
||||
const uncondCanvas = ref(null)
|
||||
const condCanvas = ref(null)
|
||||
|
||||
const currentStep = ref(0)
|
||||
const totalSteps = 20
|
||||
const isPlaying = ref(false)
|
||||
const showComparison = ref(false)
|
||||
|
||||
const marks = {
|
||||
0: '纯噪声',
|
||||
10: '中期',
|
||||
20: '清晰图'
|
||||
}
|
||||
|
||||
let animationId = null
|
||||
|
||||
// 生成目标图像(简化版)
|
||||
const generateTargetImage = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 绘制简单的目标图案
|
||||
const gradient = ctx.createLinearGradient(0, 0, 256, 256)
|
||||
gradient.addColorStop(0, '#667eea')
|
||||
gradient.addColorStop(1, '#764ba2')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, 256, 256)
|
||||
|
||||
// 添加一些形状
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(50 + i * 40, 100 + (i % 2) * 50, 30, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
return ctx.getImageData(0, 0, 256, 256)
|
||||
}
|
||||
|
||||
const targetImage = generateTargetImage()
|
||||
|
||||
// 生成噪声
|
||||
const generateNoise = (width, height, intensity) => {
|
||||
const data = new Uint8ClampedArray(width * height * 4)
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const noise = (Math.random() - 0.5) * intensity * 255
|
||||
data[i] = 128 + noise
|
||||
data[i + 1] = 128 + noise
|
||||
data[i + 2] = 128 + noise
|
||||
data[i + 3] = 255
|
||||
}
|
||||
return new ImageData(data, width, height)
|
||||
}
|
||||
|
||||
// 混合图像和噪声
|
||||
const blendWithNoise = (imageData, noiseRatio) => {
|
||||
const result = new Uint8ClampedArray(imageData.data)
|
||||
for (let i = 0; i < result.length; i += 4) {
|
||||
const noise = (Math.random() - 0.5) * noiseRatio * 255
|
||||
result[i] = Math.max(0, Math.min(255, imageData.data[i] * (1 - noiseRatio) + 128 * noiseRatio + noise))
|
||||
result[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] * (1 - noiseRatio) + 128 * noiseRatio + noise))
|
||||
result[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] * (1 - noiseRatio) + 128 * noiseRatio + noise))
|
||||
}
|
||||
return new ImageData(result, imageData.width, imageData.height)
|
||||
}
|
||||
|
||||
// 预测噪声(简化模拟)
|
||||
const predictNoise = (width, height, step) => {
|
||||
const noiseRatio = 1 - (step / totalSteps)
|
||||
return generateNoise(width, height, noiseRatio * 0.5)
|
||||
}
|
||||
|
||||
// 去噪
|
||||
const denoise = (noisyData, noiseData, step) => {
|
||||
const result = new Uint8ClampedArray(noisyData.data)
|
||||
const denoiseStrength = 0.1 + (step / totalSteps) * 0.4
|
||||
|
||||
for (let i = 0; i < result.length; i += 4) {
|
||||
// 模拟:从噪声图像中减去预测的噪声
|
||||
const targetR = targetImage.data[i]
|
||||
const targetG = targetImage.data[i + 1]
|
||||
const targetB = targetImage.data[i + 2]
|
||||
|
||||
const currentR = noisyData.data[i]
|
||||
const currentG = noisyData.data[i + 1]
|
||||
const currentB = noisyData.data[i + 2]
|
||||
|
||||
result[i] = currentR + (targetR - currentR) * denoiseStrength
|
||||
result[i + 1] = currentG + (targetG - currentG) * denoiseStrength
|
||||
result[i + 2] = currentB + (targetB - currentB) * denoiseStrength
|
||||
}
|
||||
|
||||
return new ImageData(result, noisyData.width, noisyData.height)
|
||||
}
|
||||
|
||||
// 更新显示
|
||||
const updateDisplay = () => {
|
||||
const step = currentStep.value
|
||||
const noiseRatio = 1 - (step / totalSteps)
|
||||
|
||||
// 绘制噪声图像
|
||||
const noisyCtx = noisyCanvas.value.getContext('2d')
|
||||
const noisyData = blendWithNoise(targetImage, noiseRatio)
|
||||
noisyCtx.putImageData(noisyData, 0, 0)
|
||||
|
||||
// 绘制预测的噪声
|
||||
const noiseCtx = noiseCanvas.value.getContext('2d')
|
||||
const noiseData = predictNoise(256, 256, step)
|
||||
noiseCtx.putImageData(noiseData, 0, 0)
|
||||
|
||||
// 绘制去噪结果
|
||||
const denoisedCtx = denoisedCanvas.value.getContext('2d')
|
||||
const denoisedData = denoise(noisyData, noiseData, step)
|
||||
denoisedCtx.putImageData(denoisedData, 0, 0)
|
||||
|
||||
// 更新对比图
|
||||
if (showComparison.value && uncondCanvas.value && condCanvas.value) {
|
||||
// 无条件生成(更多噪声残留)
|
||||
const uncondCtx = uncondCanvas.value.getContext('2d')
|
||||
const uncondData = blendWithNoise(targetImage, noiseRatio * 0.3)
|
||||
uncondCtx.putImageData(uncondData, 0, 0)
|
||||
|
||||
// 有条件生成(更清晰)
|
||||
const condCtx = condCanvas.value.getContext('2d')
|
||||
condCtx.putImageData(denoisedData, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const getNoiseLevel = () => {
|
||||
return Math.round((1 - currentStep.value / totalSteps) * 100)
|
||||
}
|
||||
|
||||
const getNoiseLevelType = () => {
|
||||
const level = getNoiseLevel()
|
||||
if (level > 70) return 'danger'
|
||||
if (level > 30) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
const stepForward = () => {
|
||||
if (currentStep.value < totalSteps) {
|
||||
currentStep.value++
|
||||
updateDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
const stepBackward = () => {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--
|
||||
updateDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
const togglePlay = () => {
|
||||
if (isPlaying.value) {
|
||||
stopAnimation()
|
||||
} else {
|
||||
startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
const startAnimation = () => {
|
||||
isPlaying.value = true
|
||||
const animate = () => {
|
||||
if (!isPlaying.value) return
|
||||
|
||||
if (currentStep.value >= totalSteps) {
|
||||
currentStep.value = 0
|
||||
} else {
|
||||
currentStep.value++
|
||||
}
|
||||
updateDisplay()
|
||||
|
||||
animationId = setTimeout(() => {
|
||||
requestAnimationFrame(animate)
|
||||
}, 200)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
|
||||
const stopAnimation = () => {
|
||||
isPlaying.value = false
|
||||
if (animationId) {
|
||||
clearTimeout(animationId)
|
||||
animationId = null
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
stopAnimation()
|
||||
currentStep.value = 0
|
||||
updateDisplay()
|
||||
}
|
||||
|
||||
onMounted(updateDisplay)
|
||||
onUnmounted(stopAnimation)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.unet-demo {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.display-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.display-canvas {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.noise-preview {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.timestep-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.arrow-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.model-box,
|
||||
.operation-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.model-name,
|
||||
.op-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.model-desc,
|
||||
.op-formula {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.timeline-section {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.timeline-label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compare-section {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.compare-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.compare-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.compare-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.compare-canvas {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-display {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.arrow-section {
|
||||
transform: rotate(90deg);
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user