feat(digital-human): 前端加脚本模型选择器(llm_model)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
boos
2026-06-18 11:23:19 +08:00
parent 07e31e0f7c
commit 1598e6c758
+66 -6
View File
@@ -5,6 +5,7 @@ import { getSessionToken, type SessionAudience } from '../authSession.js';
type WorkflowItem = { name: string; display_name?: string; source?: string; key?: string };
type TemplateItem = { name: string; display_name?: string; orientation?: string; size?: string; width?: number; height?: number; key?: string };
type BgmItem = { name: string; path: string; source?: string };
type MarketplaceModel = { name: string };
type GenerationMode = 'ai' | 'fixed';
type TaskStatus = 'pending' | 'running' | 'success' | 'failed' | string;
@@ -33,6 +34,7 @@ type DigitalHumanDraft = {
previewTtsText?: string;
imageWorkflow?: string;
apiVideoWorkflow?: string;
llmModel?: string;
imageWidth?: number;
imageHeight?: number;
promptPrefix?: string;
@@ -68,6 +70,13 @@ function basename(value: string): string {
return value.split('/').pop() || value;
}
const NON_TEXT_MODEL_PATTERN =
/(image|video|dall|flux|\bsd\b|sdxl|stable-?diffusion|wan|seedance|kling|banana|tts|whisper|audio|speech|voice|embedding|rerank|moderation|midjourney|\bmj\b|cogview|kolors|hunyuan-?video|vidu|pika|luma|sora|recraft|ideogram)/i;
function isTextModelName(name: string): boolean {
return Boolean(name) && !NON_TEXT_MODEL_PATTERN.test(name);
}
function readDigitalHumanDraft(): DigitalHumanDraft {
if (typeof window === 'undefined') return {};
try {
@@ -167,17 +176,32 @@ function firstString(...values: unknown[]): string | undefined {
function firstOutputUrl(value: any): string | undefined {
if (!value) return undefined;
const direct = firstString(value.video_url, value.videoUrl, value.url, value.file_url, value.fileUrl);
if (direct) return direct;
if (Array.isArray(value)) {
const archived = value
.map((item) => firstOutputUrl(item))
.find((url) => url && (url.startsWith('/api/') || !/^https?:/i.test(url)));
if (archived) return archived;
for (const item of value) {
const found = firstOutputUrl(item);
if (found) return found;
}
return undefined;
}
const direct = firstString(
value.video_url,
value.videoUrl,
value.url,
value.file_url,
value.fileUrl,
value.remote_url,
value.remoteUrl,
value.download_url,
value.downloadUrl,
);
if (direct) return direct;
if (typeof value === 'object') {
return firstOutputUrl(value.outputs)
|| firstOutputUrl(value.assets)
return firstOutputUrl(value.assets)
|| firstOutputUrl(value.outputs)
|| firstOutputUrl(value.result)
|| firstOutputUrl(value.task)
|| firstOutputUrl(value.data);
@@ -187,8 +211,16 @@ function firstOutputUrl(value: any): string | undefined {
function resolvePixelleMediaUrl(path?: string): string {
if (!path) return '';
if (/^(https?:|blob:|data:)/i.test(path) || path.startsWith('/api/')) return path;
return api.pixelle.fileUrl(path);
const normalized = String(path).trim();
if (/^(blob:|data:)/i.test(normalized) || normalized.startsWith('/api/')) return normalized;
try {
const url = new URL(normalized, typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1');
if ((url.hostname === '127.0.0.1' || url.hostname === 'localhost') && url.pathname.startsWith('/api/files/')) {
return api.pixelle.fileUrl(url.pathname.replace(/^\/api\/files\/?/, ''));
}
} catch {}
if (/^https?:/i.test(normalized)) return normalized;
return api.pixelle.fileUrl(normalized);
}
function currentAudience(): SessionAudience {
@@ -370,6 +402,8 @@ export default function DigitalHuman() {
const [previewTtsText, setPreviewTtsText] = useState(draftRef.current.previewTtsText || '你好,欢迎使用 BoosAPI 数字人。');
const [imageWorkflow, setImageWorkflow] = useState(draftRef.current.imageWorkflow || '');
const [apiVideoWorkflow, setApiVideoWorkflow] = useState(draftRef.current.apiVideoWorkflow || '');
const [llmModel, setLlmModel] = useState(draftRef.current.llmModel || '');
const [llmModels, setLlmModels] = useState<MarketplaceModel[]>([]);
const [imageWidth, setImageWidth] = useState(() => normalizeNumber(draftRef.current.imageWidth, 1024, 64));
const [imageHeight, setImageHeight] = useState(() => normalizeNumber(draftRef.current.imageHeight, 1024, 64));
const [promptPrefix, setPromptPrefix] = useState(draftRef.current.promptPrefix || DEFAULT_PROMPT_PREFIX);
@@ -414,6 +448,16 @@ export default function DigitalHuman() {
setMediaWorkflows(Array.isArray(media?.workflows) ? media.workflows : []);
setTemplates(Array.isArray(tpl?.templates) ? tpl.templates : []);
setBgmList(Array.isArray(bgm?.bgm_files) ? bgm.bgm_files : []);
void (async () => {
try {
const marketplace = (await api.getModelsMarketplace()) as { models?: MarketplaceModel[] };
const textModels = (Array.isArray(marketplace?.models) ? marketplace.models : [])
.filter((m) => m && typeof m.name === 'string' && isTextModelName(m.name));
setLlmModels(textModels);
} catch {
setLlmModels([]);
}
})();
} catch (error) {
setResourceError(getErrorMessage(error, '加载 Pixelle 资源失败'));
} finally {
@@ -463,6 +507,7 @@ export default function DigitalHuman() {
previewTtsText,
imageWorkflow,
apiVideoWorkflow,
llmModel,
imageWidth,
imageHeight,
promptPrefix,
@@ -482,6 +527,7 @@ export default function DigitalHuman() {
previewTtsText,
imageWorkflow,
apiVideoWorkflow,
llmModel,
imageWidth,
imageHeight,
promptPrefix,
@@ -579,6 +625,7 @@ export default function DigitalHuman() {
n_scenes: nScenes,
media_workflow: imageWorkflow || undefined,
api_video_workflow: apiVideoWorkflow || undefined,
llm_model: llmModel || undefined,
tts_workflow: ttsWorkflow || undefined,
frame_template: frameTemplate,
ratio: selectedTemplateSize.ratio,
@@ -614,6 +661,7 @@ export default function DigitalHuman() {
title,
nScenes,
imageWorkflow,
llmModel,
ttsWorkflow,
promptPrefix,
videoFps,
@@ -1026,6 +1074,18 @@ export default function DigitalHuman() {
))}
</select>
</label>
<label className="digital-human-field">
<span>脚本模型(可选)</span>
<select value={llmModel} onChange={(event) => setLlmModel(event.target.value)}>
<option value="">默认(deepseek-v4-flash)</option>
{llmModel && !llmModels.some((m) => m.name === llmModel) && (
<option value={llmModel}>{llmModel}</option>
)}
{llmModels.map((m) => (
<option key={m.name} value={m.name}>{m.name}</option>
))}
</select>
</label>
<div className="digital-human-form-grid">
<label className="digital-human-field">
<span>宽度</span>