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 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 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 BgmItem = { name: string; path: string; source?: string };
|
||||||
|
type MarketplaceModel = { name: string };
|
||||||
type GenerationMode = 'ai' | 'fixed';
|
type GenerationMode = 'ai' | 'fixed';
|
||||||
|
|
||||||
type TaskStatus = 'pending' | 'running' | 'success' | 'failed' | string;
|
type TaskStatus = 'pending' | 'running' | 'success' | 'failed' | string;
|
||||||
@@ -33,6 +34,7 @@ type DigitalHumanDraft = {
|
|||||||
previewTtsText?: string;
|
previewTtsText?: string;
|
||||||
imageWorkflow?: string;
|
imageWorkflow?: string;
|
||||||
apiVideoWorkflow?: string;
|
apiVideoWorkflow?: string;
|
||||||
|
llmModel?: string;
|
||||||
imageWidth?: number;
|
imageWidth?: number;
|
||||||
imageHeight?: number;
|
imageHeight?: number;
|
||||||
promptPrefix?: string;
|
promptPrefix?: string;
|
||||||
@@ -68,6 +70,13 @@ function basename(value: string): string {
|
|||||||
return value.split('/').pop() || value;
|
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 {
|
function readDigitalHumanDraft(): DigitalHumanDraft {
|
||||||
if (typeof window === 'undefined') return {};
|
if (typeof window === 'undefined') return {};
|
||||||
try {
|
try {
|
||||||
@@ -167,17 +176,32 @@ function firstString(...values: unknown[]): string | undefined {
|
|||||||
|
|
||||||
function firstOutputUrl(value: any): string | undefined {
|
function firstOutputUrl(value: any): string | undefined {
|
||||||
if (!value) return 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)) {
|
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) {
|
for (const item of value) {
|
||||||
const found = firstOutputUrl(item);
|
const found = firstOutputUrl(item);
|
||||||
if (found) return found;
|
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') {
|
if (typeof value === 'object') {
|
||||||
return firstOutputUrl(value.outputs)
|
return firstOutputUrl(value.assets)
|
||||||
|| firstOutputUrl(value.assets)
|
|| firstOutputUrl(value.outputs)
|
||||||
|| firstOutputUrl(value.result)
|
|| firstOutputUrl(value.result)
|
||||||
|| firstOutputUrl(value.task)
|
|| firstOutputUrl(value.task)
|
||||||
|| firstOutputUrl(value.data);
|
|| firstOutputUrl(value.data);
|
||||||
@@ -187,8 +211,16 @@ function firstOutputUrl(value: any): string | undefined {
|
|||||||
|
|
||||||
function resolvePixelleMediaUrl(path?: string): string {
|
function resolvePixelleMediaUrl(path?: string): string {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
if (/^(https?:|blob:|data:)/i.test(path) || path.startsWith('/api/')) return path;
|
const normalized = String(path).trim();
|
||||||
return api.pixelle.fileUrl(path);
|
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 {
|
function currentAudience(): SessionAudience {
|
||||||
@@ -370,6 +402,8 @@ export default function DigitalHuman() {
|
|||||||
const [previewTtsText, setPreviewTtsText] = useState(draftRef.current.previewTtsText || '你好,欢迎使用 BoosAPI 数字人。');
|
const [previewTtsText, setPreviewTtsText] = useState(draftRef.current.previewTtsText || '你好,欢迎使用 BoosAPI 数字人。');
|
||||||
const [imageWorkflow, setImageWorkflow] = useState(draftRef.current.imageWorkflow || '');
|
const [imageWorkflow, setImageWorkflow] = useState(draftRef.current.imageWorkflow || '');
|
||||||
const [apiVideoWorkflow, setApiVideoWorkflow] = useState(draftRef.current.apiVideoWorkflow || '');
|
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 [imageWidth, setImageWidth] = useState(() => normalizeNumber(draftRef.current.imageWidth, 1024, 64));
|
||||||
const [imageHeight, setImageHeight] = useState(() => normalizeNumber(draftRef.current.imageHeight, 1024, 64));
|
const [imageHeight, setImageHeight] = useState(() => normalizeNumber(draftRef.current.imageHeight, 1024, 64));
|
||||||
const [promptPrefix, setPromptPrefix] = useState(draftRef.current.promptPrefix || DEFAULT_PROMPT_PREFIX);
|
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 : []);
|
setMediaWorkflows(Array.isArray(media?.workflows) ? media.workflows : []);
|
||||||
setTemplates(Array.isArray(tpl?.templates) ? tpl.templates : []);
|
setTemplates(Array.isArray(tpl?.templates) ? tpl.templates : []);
|
||||||
setBgmList(Array.isArray(bgm?.bgm_files) ? bgm.bgm_files : []);
|
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) {
|
} catch (error) {
|
||||||
setResourceError(getErrorMessage(error, '加载 Pixelle 资源失败'));
|
setResourceError(getErrorMessage(error, '加载 Pixelle 资源失败'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -463,6 +507,7 @@ export default function DigitalHuman() {
|
|||||||
previewTtsText,
|
previewTtsText,
|
||||||
imageWorkflow,
|
imageWorkflow,
|
||||||
apiVideoWorkflow,
|
apiVideoWorkflow,
|
||||||
|
llmModel,
|
||||||
imageWidth,
|
imageWidth,
|
||||||
imageHeight,
|
imageHeight,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
@@ -482,6 +527,7 @@ export default function DigitalHuman() {
|
|||||||
previewTtsText,
|
previewTtsText,
|
||||||
imageWorkflow,
|
imageWorkflow,
|
||||||
apiVideoWorkflow,
|
apiVideoWorkflow,
|
||||||
|
llmModel,
|
||||||
imageWidth,
|
imageWidth,
|
||||||
imageHeight,
|
imageHeight,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
@@ -579,6 +625,7 @@ export default function DigitalHuman() {
|
|||||||
n_scenes: nScenes,
|
n_scenes: nScenes,
|
||||||
media_workflow: imageWorkflow || undefined,
|
media_workflow: imageWorkflow || undefined,
|
||||||
api_video_workflow: apiVideoWorkflow || undefined,
|
api_video_workflow: apiVideoWorkflow || undefined,
|
||||||
|
llm_model: llmModel || undefined,
|
||||||
tts_workflow: ttsWorkflow || undefined,
|
tts_workflow: ttsWorkflow || undefined,
|
||||||
frame_template: frameTemplate,
|
frame_template: frameTemplate,
|
||||||
ratio: selectedTemplateSize.ratio,
|
ratio: selectedTemplateSize.ratio,
|
||||||
@@ -614,6 +661,7 @@ export default function DigitalHuman() {
|
|||||||
title,
|
title,
|
||||||
nScenes,
|
nScenes,
|
||||||
imageWorkflow,
|
imageWorkflow,
|
||||||
|
llmModel,
|
||||||
ttsWorkflow,
|
ttsWorkflow,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
videoFps,
|
videoFps,
|
||||||
@@ -1026,6 +1074,18 @@ export default function DigitalHuman() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<div className="digital-human-form-grid">
|
||||||
<label className="digital-human-field">
|
<label className="digital-human-field">
|
||||||
<span>宽度</span>
|
<span>宽度</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user