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>
592 lines
14 KiB
Vue
592 lines
14 KiB
Vue
<!--
|
||
CFGScaleDemo.vue
|
||
CFG Scale 演示组件
|
||
|
||
用途:
|
||
展示 Classifier-Free Guidance (CFG) Scale 如何影响生成结果,帮助用户理解提示词遵循度的概念。
|
||
|
||
交互功能:
|
||
- CFG Scale 滑动调节
|
||
- 实时对比不同 CFG 值的效果
|
||
- 可视化 CFG 对图像的影响
|
||
-->
|
||
<template>
|
||
<div class="cfg-scale-demo">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="header-title">
|
||
<el-icon><ScaleToOriginal /></el-icon>
|
||
<span>⚖️ CFG Scale:提示词遵循度</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="demo-content">
|
||
<!-- CFG 控制 -->
|
||
<div class="cfg-control">
|
||
<div class="cfg-slider-section">
|
||
<div class="cfg-label">
|
||
<span>CFG Scale</span>
|
||
<el-tag
|
||
type="primary"
|
||
effect="dark"
|
||
size="large"
|
||
>
|
||
{{ cfgScale }}
|
||
</el-tag>
|
||
</div>
|
||
<el-slider
|
||
v-model="cfgScale"
|
||
:min="1"
|
||
:max="15"
|
||
:step="0.5"
|
||
show-stops
|
||
:marks="{
|
||
1: '1\n(自由创作)',
|
||
7: '7\n(平衡)',
|
||
15: '15\n(严格遵循)'
|
||
}"
|
||
/>
|
||
</div>
|
||
|
||
<div class="cfg-presets">
|
||
<el-button
|
||
v-for="preset in cfgPresets"
|
||
:key="preset.value"
|
||
:type="cfgScale === preset.value ? 'primary' : ''"
|
||
size="small"
|
||
@click="cfgScale = preset.value"
|
||
>
|
||
{{ preset.label }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 对比展示 -->
|
||
<div class="comparison-display">
|
||
<div class="comparison-item">
|
||
<div class="item-label">
|
||
<el-tag type="info">
|
||
无条件生成
|
||
</el-tag>
|
||
<span class="cfg-value">CFG = 1</span>
|
||
</div>
|
||
<canvas
|
||
ref="uncondCanvas"
|
||
width="200"
|
||
height="200"
|
||
class="comparison-canvas"
|
||
/>
|
||
<div class="item-desc">
|
||
忽略提示词,自由发挥
|
||
</div>
|
||
</div>
|
||
|
||
<div class="comparison-arrow">
|
||
<el-icon :size="32">
|
||
<ArrowRight />
|
||
</el-icon>
|
||
<div class="guidance-formula">
|
||
<div class="formula">
|
||
输出 = 无条件 + CFG × (有条件 - 无条件)
|
||
</div>
|
||
<div class="formula-desc">
|
||
CFG 越大,提示词影响越强
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="comparison-item">
|
||
<div class="item-label">
|
||
<el-tag type="success">
|
||
当前设置
|
||
</el-tag>
|
||
<span class="cfg-value">CFG = {{ cfgScale }}</span>
|
||
</div>
|
||
<canvas
|
||
ref="currentCanvas"
|
||
width="200"
|
||
height="200"
|
||
class="comparison-canvas"
|
||
/>
|
||
<div class="item-desc">
|
||
{{ getCfgDescription() }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CFG 效果展示 -->
|
||
<div class="cfg-effects">
|
||
<div class="effects-title">
|
||
不同 CFG 值的效果对比
|
||
</div>
|
||
<div class="effects-grid">
|
||
<div
|
||
v-for="effect in cfgEffects"
|
||
:key="effect.value"
|
||
class="effect-item"
|
||
:class="{ active: cfgScale === effect.value }"
|
||
@click="cfgScale = effect.value"
|
||
>
|
||
<canvas
|
||
:ref="el => setEffectCanvas(el, effect.value)"
|
||
width="120"
|
||
height="120"
|
||
class="effect-canvas"
|
||
/>
|
||
<div class="effect-label">
|
||
CFG {{ effect.value }}
|
||
</div>
|
||
<div class="effect-desc">
|
||
{{ effect.desc }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐设置 -->
|
||
<div class="recommendations">
|
||
<div class="rec-title">
|
||
🎯 推荐设置
|
||
</div>
|
||
<div class="rec-grid">
|
||
<div class="rec-item">
|
||
<div class="rec-scenario">
|
||
创意探索
|
||
</div>
|
||
<div class="rec-value">
|
||
CFG 3-5
|
||
</div>
|
||
<div class="rec-desc">
|
||
给 AI 更多自由,适合艺术探索
|
||
</div>
|
||
</div>
|
||
<div class="rec-item">
|
||
<div class="rec-scenario">
|
||
平衡模式
|
||
</div>
|
||
<div class="rec-value">
|
||
CFG 7-9
|
||
</div>
|
||
<div class="rec-desc">
|
||
大多数场景的最佳选择
|
||
</div>
|
||
</div>
|
||
<div class="rec-item">
|
||
<div class="rec-scenario">
|
||
精确控制
|
||
</div>
|
||
<div class="rec-value">
|
||
CFG 12-15
|
||
</div>
|
||
<div class="rec-desc">
|
||
严格遵循提示词,但可能过饱和
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-box">
|
||
<p>
|
||
<span class="icon">💡</span>
|
||
<strong>CFG Scale 原理:</strong>
|
||
CFG (Classifier-Free Guidance) 控制生成结果对提示词的遵循程度。值越高,图像越符合提示词描述,但过高会导致图像过饱和或失真。
|
||
</p>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, watch } from 'vue'
|
||
import { ScaleToOriginal, ArrowRight } from '@element-plus/icons-vue'
|
||
|
||
const cfgScale = ref(7.5)
|
||
const uncondCanvas = ref(null)
|
||
const currentCanvas = ref(null)
|
||
const effectCanvases = ref({})
|
||
|
||
const cfgPresets = [
|
||
{ label: '自由 (3)', value: 3 },
|
||
{ label: '平衡 (7)', value: 7 },
|
||
{ label: '严格 (12)', value: 12 }
|
||
]
|
||
|
||
const cfgEffects = [
|
||
{ value: 1, desc: '完全自由' },
|
||
{ value: 3, desc: '创意优先' },
|
||
{ value: 5, desc: '轻度引导' },
|
||
{ value: 7, desc: '平衡' },
|
||
{ value: 9, desc: '严格遵循' },
|
||
{ value: 12, desc: '非常严格' },
|
||
{ value: 15, desc: '过度饱和' }
|
||
]
|
||
|
||
const setEffectCanvas = (el, value) => {
|
||
if (el) {
|
||
effectCanvases.value[value] = el
|
||
}
|
||
}
|
||
|
||
// 绘制目标图像
|
||
const drawTargetImage = (ctx, width, height, cfgValue) => {
|
||
// 基础图像(提示词:一只蓝色的猫)
|
||
const baseColor = { r: 100, g: 150, b: 200 }
|
||
|
||
// 根据 CFG 值调整颜色饱和度
|
||
const saturationBoost = Math.min((cfgValue - 1) / 7, 1.5)
|
||
const color = {
|
||
r: Math.min(255, baseColor.r + saturationBoost * 50),
|
||
g: Math.max(0, baseColor.g - saturationBoost * 30),
|
||
b: Math.min(255, baseColor.b + saturationBoost * 30)
|
||
}
|
||
|
||
// 背景
|
||
ctx.fillStyle = '#f0f0f0'
|
||
ctx.fillRect(0, 0, width, height)
|
||
|
||
// 猫的形状
|
||
ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`
|
||
|
||
// 身体
|
||
ctx.beginPath()
|
||
ctx.ellipse(width / 2, height * 0.65, width * 0.25, height * 0.2, 0, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
|
||
// 头
|
||
ctx.beginPath()
|
||
ctx.arc(width / 2, height * 0.4, width * 0.18, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
|
||
// 耳朵
|
||
ctx.beginPath()
|
||
ctx.moveTo(width * 0.35, height * 0.3)
|
||
ctx.lineTo(width * 0.3, height * 0.15)
|
||
ctx.lineTo(width * 0.42, height * 0.25)
|
||
ctx.fill()
|
||
|
||
ctx.beginPath()
|
||
ctx.moveTo(width * 0.65, height * 0.3)
|
||
ctx.lineTo(width * 0.7, height * 0.15)
|
||
ctx.lineTo(width * 0.58, height * 0.25)
|
||
ctx.fill()
|
||
|
||
// 眼睛
|
||
ctx.fillStyle = '#fff'
|
||
ctx.beginPath()
|
||
ctx.ellipse(width * 0.45, height * 0.38, width * 0.05, height * 0.04, 0, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
ctx.beginPath()
|
||
ctx.ellipse(width * 0.55, height * 0.38, width * 0.05, height * 0.04, 0, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
|
||
// 瞳孔
|
||
ctx.fillStyle = '#000'
|
||
ctx.beginPath()
|
||
ctx.arc(width * 0.45, height * 0.38, width * 0.025, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
ctx.beginPath()
|
||
ctx.arc(width * 0.55, height * 0.38, width * 0.025, 0, Math.PI * 2)
|
||
ctx.fill()
|
||
|
||
// 添加噪声(模拟低 CFG 的自由度)
|
||
if (cfgValue < 5) {
|
||
const imageData = ctx.getImageData(0, 0, width, height)
|
||
const noiseAmount = (5 - cfgValue) / 5 * 30
|
||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||
const noise = (Math.random() - 0.5) * noiseAmount
|
||
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise))
|
||
imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise))
|
||
imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise))
|
||
}
|
||
ctx.putImageData(imageData, 0, 0)
|
||
}
|
||
|
||
// 添加过饱和效果(高 CFG)
|
||
if (cfgValue > 10) {
|
||
const imageData = ctx.getImageData(0, 0, width, height)
|
||
const oversaturation = (cfgValue - 10) / 5
|
||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||
// 增强对比度
|
||
const avg = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3
|
||
imageData.data[i] = Math.min(255, imageData.data[i] + (imageData.data[i] - avg) * oversaturation)
|
||
imageData.data[i + 1] = Math.min(255, imageData.data[i + 1] + (imageData.data[i + 1] - avg) * oversaturation)
|
||
imageData.data[i + 2] = Math.min(255, imageData.data[i + 2] + (imageData.data[i + 2] - avg) * oversaturation)
|
||
}
|
||
ctx.putImageData(imageData, 0, 0)
|
||
}
|
||
}
|
||
|
||
const getCfgDescription = () => {
|
||
if (cfgScale.value <= 3) return '自由创作,AI 有更多发挥空间'
|
||
if (cfgScale.value <= 7) return '平衡模式,兼顾创意和遵循'
|
||
if (cfgScale.value <= 10) return '严格遵循提示词'
|
||
return '过度控制,可能导致图像失真'
|
||
}
|
||
|
||
const updateDisplay = () => {
|
||
// 更新无条件生成
|
||
if (uncondCanvas.value) {
|
||
const ctx = uncondCanvas.value.getContext('2d')
|
||
drawTargetImage(ctx, 200, 200, 1)
|
||
}
|
||
|
||
// 更新当前设置
|
||
if (currentCanvas.value) {
|
||
const ctx = currentCanvas.value.getContext('2d')
|
||
drawTargetImage(ctx, 200, 200, cfgScale.value)
|
||
}
|
||
|
||
// 更新效果网格
|
||
cfgEffects.forEach(effect => {
|
||
const canvas = effectCanvases.value[effect.value]
|
||
if (canvas) {
|
||
const ctx = canvas.getContext('2d')
|
||
drawTargetImage(ctx, 120, 120, effect.value)
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(updateDisplay)
|
||
watch(cfgScale, updateDisplay)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.cfg-scale-demo {
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.demo-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.cfg-control {
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 6px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.cfg-slider-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.cfg-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.cfg-label span {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.cfg-presets {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.comparison-display {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 24px;
|
||
flex-wrap: wrap;
|
||
padding: 16px;
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.comparison-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.item-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.cfg-value {
|
||
font-weight: 600;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.comparison-canvas {
|
||
width: 180px;
|
||
height: 180px;
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
border: 2px solid var(--vp-c-divider);
|
||
}
|
||
|
||
.item-desc {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-3);
|
||
text-align: center;
|
||
}
|
||
|
||
.comparison-arrow {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: var(--vp-c-brand);
|
||
}
|
||
|
||
.guidance-formula {
|
||
text-align: center;
|
||
max-width: 200px;
|
||
}
|
||
|
||
.formula {
|
||
font-size: 0.75rem;
|
||
font-family: var(--vp-font-family-mono);
|
||
background: var(--vp-c-bg);
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.formula-desc {
|
||
font-size: 0.75rem;
|
||
color: var(--vp-c-text-3);
|
||
}
|
||
|
||
.cfg-effects {
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 6px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.effects-title {
|
||
font-weight: 500;
|
||
margin-bottom: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.effects-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.effect-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 2px solid transparent;
|
||
}
|
||
|
||
.effect-item:hover {
|
||
border-color: var(--vp-c-brand);
|
||
}
|
||
|
||
.effect-item.active {
|
||
border-color: var(--vp-c-brand);
|
||
background: var(--vp-c-bg-mute);
|
||
}
|
||
|
||
.effect-canvas {
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.effect-label {
|
||
font-weight: 500;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.effect-desc {
|
||
font-size: 0.75rem;
|
||
color: var(--vp-c-text-3);
|
||
text-align: center;
|
||
}
|
||
|
||
.recommendations {
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 6px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.rec-title {
|
||
font-weight: 500;
|
||
margin-bottom: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.rec-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.rec-item {
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.rec-scenario {
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.rec-value {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
color: var(--vp-c-brand);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.rec-desc {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-3);
|
||
}
|
||
|
||
.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: 640px) {
|
||
.comparison-display {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.comparison-arrow {
|
||
transform: rotate(90deg);
|
||
margin: 8px 0;
|
||
}
|
||
}
|
||
</style>
|