概述
在 AI 应用中,不同模型(LLM 大语言模型、文生图 Stable Diffusion、文生视频 SVD)的参数体系差异巨大。本文将构建一套 通用参数面板架构:以 TypeScript 配置表驱动动态渲染,统一处理温度、TopP、采样步数、CFG、帧率等参数,同时涵盖参数联动、互斥校验、非法输入拦截和预设模板持久化。
1. 参数体系:三类模型的差异化字段
LLM(大语言模型)
temperature: 0~2(默认 0.7)
top_p: 0~1(默认 0.9)
max_tokens: 1~32768
frequency_penalty: -2~2
presence_penalty: -2~2
stop_sequences: string[]
stream: boolean
文生图(SD / SDXL)
sampler: 'euler_a' | 'ddim' | 'dpm++_2m' | 'lcm'
steps: 1~150(默认 25)
cfg_scale: 1~30(默认 7)
seed: number(-1 = 随机)
width / height: 512~2048
high_res_fix: boolean
文生视频(SVD)
frames: 14~30
fps: 8~30
motion_bucket_id: 0~255
interpolation_mode: 'none' | 'frame_blend' | 'optical_flow'
duration: 自动计算 = frames / fps
2. 架构设计:配置表驱动渲染
核心理念:一份统一配置表,切换模型类型时动态加载对应参数组。
// ===== 参数配置表声明 =====
interface ParamConfig {
key: string;
label: string;
type: 'slider' | 'select' | 'toggle' | 'number' | 'tag-input';
defaultValue: number | string | boolean | string[];
min?: number;
max?: number;
step?: number;
options?: { label: string; value: string | number }[];
// 互斥规则
disabledWhen?: (params: Record<string, any>) => boolean;
// 联动计算
computedBy?: (params: Record<string, any>) => number;
}
interface ModelPreset {
id: string;
name: string;
type: 'llm' | 'txt2img' | 'txt2video';
params: ParamConfig[];
}
// ===== 以 SD 为例的配置声明 =====
const SD_PRESET: ModelPreset = {
id: 'sd-xl',
name: 'SD XL',
type: 'txt2img',
params: [
{
key: 'sampler',
label: '采样器',
type: 'select',
defaultValue: 'dpm++_2m',
options: [
{ label: 'Euler A', value: 'euler_a' },
{ label: 'DDIM', value: 'ddim' },
{ label: 'DPM++ 2M', value: 'dpm++_2m' },
{ label: 'LCM', value: 'lcm' },
],
},
{
key: 'steps',
label: '采样步数',
type: 'slider',
defaultValue: 25,
min: 1,
max: 150,
step: 1,
},
{
key: 'cfg_scale',
label: 'CFG (提示词强度)',
type: 'slider',
defaultValue: 7,
min: 1,
max: 30,
step: 0.5,
},
{
key: 'seed',
label: '随机种子',
type: 'number',
defaultValue: -1,
min: -1,
max: 4294967295,
},
{
key: 'high_res_fix',
label: '高清修复',
type: 'toggle',
defaultValue: false,
// 部分采样器不支持高清修复
disabledWhen: (p) => ['ddim', 'lcm'].includes(p.sampler as string),
},
],
};
3. 核心组件:参数面板渲染器
// ===== ParameterPanel.tsx =====
import React, { useEffect, useState, useCallback } from 'react';
interface Props {
preset: ModelPreset;
onChange: (params: Record<string, any>) => void;
// 外部可控状态(用于预设加载)
externalValues?: Record<string, any>;
}
export default function ParameterPanel({ preset, onChange, externalValues }: Props) {
const [params, setParams] = useState<Record<string, any>>({});
// 初始化默认值
useEffect(() => {
const defaults: Record<string, any> = {};
preset.params.forEach((p) => {
defaults[p.key] = p.defaultValue;
});
setParams(externalValues ?? defaults);
}, [preset.id, externalValues]);
// 通知外部变化
useEffect(() => {
onChange(params);
}, [params, onChange]);
// 安全更新单个参数
const updateParam = useCallback((key: string, rawValue: any) => {
const cfg = preset.params.find((p) => p.key === key);
if (!cfg) return;
let value = rawValue;
// ---- 非法输入处理 ----
if (cfg.type === 'number' || cfg.type === 'slider') {
const num = Number(rawValue);
if (isNaN(num)) {
// 非数字:恢复默认值
value = cfg.defaultValue;
} else {
// 边界吸附
if (cfg.min !== undefined && num < cfg.min) value = cfg.min;
else if (cfg.max !== undefined && num > cfg.max) value = cfg.max;
else value = num;
}
}
setParams((prev) => {
const next = { ...prev, [key]: value };
// ---- 参数联动 ----
preset.params.forEach((p) => {
if (p.computedBy) {
next[p.key] = p.computedBy(next);
}
});
return next;
});
}, [preset]);
// 渲染单个控件
const renderControl = (cfg: ParamConfig) => {
const value = params[cfg.key];
const disabled = cfg.disabledWhen?.(params) ?? false;
switch (cfg.type) {
case 'slider':
return (
<div key={cfg.key} className="param-row">
<label>{cfg.label}: {value}</label>
<input
type="range"
min={cfg.min}
max={cfg.max}
step={cfg.step}
value={value}
disabled={disabled}
onChange={(e) => updateParam(cfg.key, e.target.value)}
/>
{disabled && <span className="disabled-hint">该采样器不支持高清修复</span>}
</div>
);
case 'select':
return (
<div key={cfg.key} className="param-row">
<label>{cfg.label}</label>
<select
value={value as string}
disabled={disabled}
onChange={(e) => updateParam(cfg.key, e.target.value)}
>
{cfg.options?.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
);
case 'toggle':
return (
<div key={cfg.key} className="param-row">
<label>
<input
type="checkbox"
checked={!!value}
disabled={disabled}
onChange={(e) => updateParam(cfg.key, e.target.checked)}
/>{' '}
{cfg.label}
</label>
{disabled && <span className="disabled-hint">当前采样器不可用</span>}
</div>
);
case 'number':
return (
<div key={cfg.key} className="param-row">
<label>{cfg.label}</label>
<input
type="number"
min={cfg.min}
max={cfg.max}
step={cfg.step}
value={value}
disabled={disabled}
onChange={(e) => updateParam(cfg.key, e.target.value)}
onBlur={(e) => {
const num = Number(e.target.value);
if (isNaN(num) || (cfg.min !== undefined && num < cfg.min)) {
updateParam(cfg.key, cfg.min ?? cfg.defaultValue);
}
if (cfg.max !== undefined && num > cfg.max) {
updateParam(cfg.key, cfg.max);
}
}}
/>
</div>
);
case 'tag-input':
return (
<div key={cfg.key} className="param-row">
<label>{cfg.label}</label>
<div className="tag-list">
{(value as string[]).map((tag, i) => (
<span key={i} className="tag">
{tag}
<button onClick={() => {
const next = (value as string[]).filter((_, j) => j !== i);
updateParam(cfg.key, next);
}}>×</button>
</span>
))}
<input
placeholder="输入停止词,回车添加"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
const next = [...(value as string[]), e.currentTarget.value.trim()];
updateParam(cfg.key, next);
e.currentTarget.value = '';
}
}}
/>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="parameter-panel">
<h3>{preset.name} 参数</h3>
{preset.params.map(renderControl)}
</div>
);
}
4. 预设模板与 localStorage 持久化
// ===== preset-manager.ts =====
interface SavedTemplate {
id: string;
name: string;
modelType: string;
params: Record<string, any>;
}
const STORAGE_KEY = 'ai-param-templates';
export function loadTemplates(): SavedTemplate[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}
export function saveTemplate(template: SavedTemplate): void {
const list = loadTemplates();
const idx = list.findIndex((t) => t.id === template.id);
if (idx >= 0) {
list[idx] = template;
} else {
list.push(template);
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
export function deleteTemplate(id: string): void {
const list = loadTemplates().filter((t) => t.id !== id);
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
export function importTemplateFromJSON(json: string): SavedTemplate | null {
try {
const obj = JSON.parse(json);
if (!obj.name || !obj.params) return null;
return { id: Date.now().toString(), name: obj.name, modelType: obj.type ?? 'llm', params: obj.params };
} catch {
return null;
}
}
export function exportTemplateToJSON(template: SavedTemplate): string {
return JSON.stringify({ name: template.name, type: template.modelType, params: template.params }, null, 2);
}
5. 视频模型参数联动:帧率 × 帧数 = 时长
// SVD 模型的联动计算示例
const SVD_PRESET: ModelPreset = {
id: 'svd',
name: 'SVD (文生视频)',
type: 'txt2video',
params: [
{
key: 'frames',
label: '帧数',
type: 'slider',
defaultValue: 25,
min: 14,
max: 30,
step: 1,
},
{
key: 'fps',
label: '帧率',
type: 'slider',
defaultValue: 8,
min: 4,
max: 30,
step: 1,
},
{
key: 'duration',
label: '视频时长 (秒)',
type: 'number',
defaultValue: 3.1,
// 只读显示,由 frames / fps 自动计算
computedBy: (p) => {
const frames = Number(p.frames) || 25;
const fps = Number(p.fps) || 8;
return Math.round((frames / fps) * 10) / 10;
},
},
],
};
6. 最终提交:序列化参数
// 面板层负责把参数打包成 API 要求的格式
function buildSubmitPayload(preset: ModelPreset, params: Record<string, any>) {
return {
model_type: preset.type,
model_id: preset.id,
params: { ...params },
// 自动移除 -1 的随机 seed
seed: params.seed === -1 ? Math.floor(Math.random() * 4294967295) : params.seed,
};
}
总结
- 配置表驱动:一份
ParamConfig声明搞定所有模型,切换模型类型自动刷出对应控件 - 校验三件套:非法输入→回填默认值、超出边界→自动吸附、互斥字段→动态 disabled
- 联动计算:
computedBy在每次setParams时自动触发,帧率变化自动换算时长 - 预设持久化:localStorage 存模板,支持新建/加载/删除/导入导出 JSON
- 面试亮点:能清晰对比 LLM / SD / SVD 三类模型的参数差异化处理
Comments 留言讨论
还没有评论,来抢个沙发,聊聊你的看法~