0eba9e87e9
- Disable formatting rules (handled by Prettier) - Relaxed strict Vue/JS rules for demo code compatibility - Fix syntax errors in ApiPlayground and VoiceCloningDemo - Fix duplicate else-if condition in ApiPlayground - Fix Promise executor async pattern in AutoregressiveAudioDemo - Add TypeScript file support to ESLint config Warnings reduced from 295 to 251 problems. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
331 lines
7.5 KiB
Vue
331 lines
7.5 KiB
Vue
<template>
|
||
<div class="latent-space-viz">
|
||
<div class="viz-card">
|
||
<!-- Left: The Output (Pixel Space) -->
|
||
<div class="preview-section">
|
||
<div
|
||
class="emoji-display"
|
||
:style="{ transform: `scale(${1 + zoomLevel})` }"
|
||
>
|
||
{{ currentEmoji }}
|
||
</div>
|
||
<div class="label">
|
||
像素空间 (Pixel Space)
|
||
</div>
|
||
<div class="sub-label">
|
||
最终看到的图像
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Center: The Mechanism -->
|
||
<div class="connection">
|
||
<div class="arrow">
|
||
← 映射 →
|
||
</div>
|
||
<div class="vae-tag">
|
||
VAE Decoder
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: The Input (Latent Space) -->
|
||
<div class="control-section">
|
||
<div
|
||
ref="gridRef"
|
||
class="latent-grid"
|
||
@mousedown="startDrag"
|
||
@touchstart="startDrag"
|
||
>
|
||
<div class="grid-lines" />
|
||
<div class="axis-label x-axis">
|
||
开心值 (Happiness)
|
||
</div>
|
||
<div class="axis-label y-axis">
|
||
惊讶值 (Surprise)
|
||
</div>
|
||
|
||
<!-- The Latent Point -->
|
||
<div
|
||
class="latent-point"
|
||
:style="{ left: `${point.x}%`, top: `${point.y}%` }"
|
||
>
|
||
<div class="tooltip">
|
||
Latent Vector: [{{ ((point.x-50)/50).toFixed(1) }}, {{ ((50-point.y)/50).toFixed(1) }}]
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="label">
|
||
潜空间 (Latent Space)
|
||
</div>
|
||
<div class="sub-label">
|
||
拖动红点改变特征
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-bar">
|
||
<span class="icon">💡</span>
|
||
<span>
|
||
<strong>核心原理:</strong>
|
||
在像素空间里修改图片很难(要改几千个像素)。但在潜空间里,我们只需要修改两个坐标(开心值、惊讶值),就能生成完全不同的表情。这就是 AI "画画" 的本质——在数学空间里找坐标。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
|
||
const gridRef = ref(null)
|
||
const isDragging = ref(false)
|
||
const point = ref({ x: 50, y: 50 }) // Percentage 0-100
|
||
const zoomLevel = ref(0)
|
||
|
||
// Emoji map based on quadrants
|
||
// X: Unhappy -> Happy
|
||
// Y: Calm -> Surprised (Top is 0 in CSS, so small Y is high surprise?)
|
||
// Let's map:
|
||
// X (0-100): Sad -> Happy
|
||
// Y (0-100): Surprised -> Sleepy (Top is 0, so 0 is Surprised, 100 is Sleepy)
|
||
|
||
const currentEmoji = computed(() => {
|
||
const x = point.value.x // 0 (Sad) to 100 (Happy)
|
||
const y = point.value.y // 0 (Surprised) to 100 (Sleepy)
|
||
|
||
if (x < 33) { // Sad Zone
|
||
if (y < 33) return '😨' // Sad + Surprised = Fear
|
||
if (y > 66) return '😪' // Sad + Sleepy = Tired
|
||
return '😢' // Just Sad
|
||
} else if (x > 66) { // Happy Zone
|
||
if (y < 33) return '🤩' // Happy + Surprised = Starstruck
|
||
if (y > 66) return '😌' // Happy + Sleepy = Relieved
|
||
return '😃' // Just Happy
|
||
} else { // Neutral Zone
|
||
if (y < 33) return '😮' // Neutral + Surprised
|
||
if (y > 66) return '😴' // Neutral + Sleepy
|
||
return '😐' // Just Neutral
|
||
}
|
||
})
|
||
|
||
const handleMove = (event) => {
|
||
if (!isDragging.value) return
|
||
|
||
const grid = gridRef.value.getBoundingClientRect()
|
||
const clientX = event.touches ? event.touches[0].clientX : event.clientX
|
||
const clientY = event.touches ? event.touches[0].clientY : event.clientY
|
||
|
||
let newX = ((clientX - grid.left) / grid.width) * 100
|
||
let newY = ((clientY - grid.top) / grid.height) * 100
|
||
|
||
// Clamp
|
||
newX = Math.max(0, Math.min(100, newX))
|
||
newY = Math.max(0, Math.min(100, newY))
|
||
|
||
point.value = { x: newX, y: newY }
|
||
}
|
||
|
||
const startDrag = (event) => {
|
||
isDragging.value = true
|
||
handleMove(event)
|
||
// Prevent default to stop scrolling on mobile
|
||
if (event.type === 'touchstart') event.preventDefault()
|
||
}
|
||
|
||
const stopDrag = () => {
|
||
isDragging.value = false
|
||
}
|
||
|
||
onMounted(() => {
|
||
window.addEventListener('mousemove', handleMove)
|
||
window.addEventListener('mouseup', stopDrag)
|
||
window.addEventListener('touchmove', handleMove)
|
||
window.addEventListener('touchend', stopDrag)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('mousemove', handleMove)
|
||
window.removeEventListener('mouseup', stopDrag)
|
||
window.removeEventListener('touchmove', handleMove)
|
||
window.removeEventListener('touchend', stopDrag)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.latent-space-viz {
|
||
margin: 20px 0;
|
||
font-family: var(--vp-font-family-base);
|
||
}
|
||
|
||
.viz-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: var(--vp-c-bg-soft);
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.preview-section, .control-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.emoji-display {
|
||
font-size: 80px;
|
||
line-height: 1;
|
||
height: 100px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
cursor: default;
|
||
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.1));
|
||
}
|
||
|
||
.latent-grid {
|
||
width: 200px;
|
||
height: 200px;
|
||
background: var(--vp-c-bg);
|
||
border: 2px solid var(--vp-c-brand);
|
||
border-radius: 12px;
|
||
position: relative;
|
||
cursor: crosshair;
|
||
overflow: hidden;
|
||
box-shadow: inset 0 2px 8px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.grid-lines {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-image:
|
||
linear-gradient(var(--vp-c-divider) 1px, transparent 1px),
|
||
linear-gradient(90deg, var(--vp-c-divider) 1px, transparent 1px);
|
||
background-size: 20px 20px;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.latent-point {
|
||
width: 20px;
|
||
height: 20px;
|
||
background: var(--vp-c-brand);
|
||
border: 3px solid #fff;
|
||
border-radius: 50%;
|
||
position: absolute;
|
||
transform: translate(-50%, -50%);
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
transition: transform 0.1s;
|
||
}
|
||
|
||
.latent-point:hover {
|
||
transform: translate(-50%, -50%) scale(1.2);
|
||
}
|
||
|
||
.tooltip {
|
||
position: absolute;
|
||
bottom: 25px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0,0,0,0.8);
|
||
color: #fff;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
white-space: nowrap;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.latent-point:hover .tooltip,
|
||
.latent-point:active .tooltip {
|
||
opacity: 1;
|
||
}
|
||
|
||
.axis-label {
|
||
position: absolute;
|
||
font-size: 10px;
|
||
color: var(--vp-c-text-2);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.x-axis {
|
||
bottom: 4px;
|
||
right: 8px;
|
||
}
|
||
|
||
.y-axis {
|
||
top: 8px;
|
||
left: 8px;
|
||
writing-mode: vertical-rl;
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.connection {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
color: var(--vp-c-text-2);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.arrow {
|
||
margin-bottom: 4px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.vae-tag {
|
||
background: var(--vp-c-brand-dimm);
|
||
color: var(--vp-c-brand-dark);
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.label {
|
||
margin-top: 12px;
|
||
font-weight: 600;
|
||
color: var(--vp-c-text-1);
|
||
}
|
||
|
||
.sub-label {
|
||
font-size: 12px;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.info-bar {
|
||
margin-top: 16px;
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 12px 16px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--vp-c-text-2);
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.icon {
|
||
font-size: 16px;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.viz-card {
|
||
flex-direction: column-reverse;
|
||
}
|
||
|
||
.connection {
|
||
transform: rotate(90deg);
|
||
margin: 10px 0;
|
||
}
|
||
}
|
||
</style>
|