feat(digital-human): 前端加脚本模型选择器(llm_model)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user