feat(web): 角色与音频工作台 - 完善素材选择、生成参数与同步交互

This commit is contained in:
ddcat666
2026-10-01 09:43:02 +08:00
parent 3aa874a70c
commit 12125e18f6
77 changed files with 3641 additions and 721 deletions

View File

@@ -27,7 +27,7 @@ export type AgentProfileView = {
layers: AgentProfileLayer[];
};
export type AgentApprovalPreviewOperation = "add_node" | "update_node" | "connect_nodes" | "arrange_nodes" | "generate_media" | "create_storyboard" | "edit_storyboard" | "plan_step";
export type AgentApprovalPreviewOperation = "add_node" | "update_node" | "connect_nodes" | "arrange_nodes" | "generate_media" | "create_storyboard" | "edit_storyboard" | "create_character" | "plan_step";
export type AgentApprovalPreviewItem = {
operation: AgentApprovalPreviewOperation;

View File

@@ -608,7 +608,7 @@ export function updateAdminDrawingEngineSetting(input: Pick<CanvasDrawingEngineS
return http.patch<{ setting: CanvasDrawingEngineSetting }>("/admin/settings/drawing-engine", input);
}
export type AdminApiLogParams = AdminListParams & { recordType?: "request" | "download" | "all" };
export type AdminApiLogParams = AdminListParams & { recordType?: "request" | "download" | "all"; capability?: "text" | "image" | "video" | "audio" };
export function listAdminApiLogs(params: AdminApiLogParams = {}) {
return http.get<{ logs: ApiCallLog[]; total: number; page: number; pageSize: number }>("/admin/api-logs", { params });
@@ -618,8 +618,16 @@ export function getAdminApiLog(id: string) {
return http.get<{ log: ApiCallLog }>(`/admin/api-logs/${encodeURIComponent(id)}`);
}
export function queryAdminApiLogTask(id: string) {
return http.post<AdminProviderTaskQueryResult>(`/admin/api-logs/${encodeURIComponent(id)}/query-task`);
export function queryAdminApiLogTask(id: string, providerRequestId?: string) {
return http.post<AdminProviderTaskQueryResult>(`/admin/api-logs/${encodeURIComponent(id)}/query-task`, providerRequestId ? { providerRequestId } : undefined, { timeout: 120_000 });
}
export function recoverAdminApiLogVideoByURL(id: string, url: string, providerRequestId?: string) {
return http.post<AdminProviderTaskQueryResult>(`/admin/api-logs/${encodeURIComponent(id)}/recover-url`, { url, providerRequestId }, { timeout: 120_000 });
}
export function batchRecoverAdminApiLogs(ids: string[]) {
return http.post<{ items: Array<{ logId: string; recovered: boolean; providerStatus?: string; error?: string }>; success: number; failed: number; skipped: number }>("/admin/api-logs/recover-batch", { ids }, { timeout: 180_000 });
}
export async function exportAdminApiLogs(params: AdminApiLogParams & { ids?: string[] } = {}) {

View File

@@ -1,3 +1,4 @@
import { normalizeAudioFormatForConfig, normalizeAudioVoiceForConfig } from "@/lib/audio-generation";
import { getMediaBlob } from "@/services/file-storage";
import { getImageBlob } from "@/services/image-storage";
import { resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
@@ -306,7 +307,7 @@ function backendGenerationTaskInput(options: BackendGenerationTaskOptions, prepa
referenceVideos: prepared.referenceVideos,
referenceAudios: prepared.referenceAudios,
mask: prepared.mask,
metadata: generationMetadata(config, mode, {
metadata: generationMetadata(config, {
...metadata,
...(options.clientOperationId ? { clientOperationId: options.clientOperationId } : {}),
...(options.retryOf ? { retryOf: options.retryOf } : {}),
@@ -316,7 +317,7 @@ function backendGenerationTaskInput(options: BackendGenerationTaskOptions, prepa
};
}
function generationMetadata(config: AiConfig, mode: BackendGenerationMode, metadata?: Record<string, unknown>) {
function generationMetadata(config: AiConfig, metadata?: Record<string, unknown>) {
const channel = resolveModelChannel(config, config.model);
const model = modelOptionName(config.model);
const modelCost = channel.modelCosts?.find((item) => item.model === model);
@@ -329,20 +330,8 @@ function generationMetadata(config: AiConfig, mode: BackendGenerationMode, metad
? existing[protocol] as Record<string, unknown>
: {};
const defaults = modelCost?.defaultOptions && typeof modelCost.defaultOptions === "object" ? modelCost.defaultOptions : {};
const audioOptions = mode === "audio" ? {
emo_control_method: config.audioEmotionControlMethod || "与音色参考音频相同",
emo_random: config.audioEmotionRandom === "true",
emo_happy: Number(config.audioEmotionHappy || 0),
emo_angry: Number(config.audioEmotionAngry || 0),
emo_sad: Number(config.audioEmotionSad || 0),
emo_afraid: Number(config.audioEmotionAfraid || 0),
emo_disgusted: Number(config.audioEmotionDisgusted || 0),
emo_melancholic: Number(config.audioEmotionMelancholic || 0),
emo_surprised: Number(config.audioEmotionSurprised || 0),
emo_calm: Number(config.audioEmotionCalm || 0),
} : {};
if (!Object.keys(defaults).length && !Object.keys(audioOptions).length && !Object.keys(namespace).length) return metadata;
return { ...metadata, providerOptions: { ...existing, [protocol]: { ...defaults, ...audioOptions, ...namespace } } };
if (!Object.keys(defaults).length && !Object.keys(namespace).length) return metadata;
return { ...metadata, providerOptions: { ...existing, [protocol]: { ...defaults, ...namespace } } };
}
async function prepareBackendMediaReference(media: ReferenceVideo | ReferenceAudio) {
@@ -420,9 +409,11 @@ export function backendProviderConfig(config: AiConfig, mode: BackendGenerationM
videoGenerateAudio: config.videoGenerateAudio,
videoWatermark: config.videoWatermark,
videoArkPrivateAssetUpload: config.videoArkPrivateAssetUpload,
audioVoice: config.audioVoice,
audioFormat: config.audioFormat,
audioVoice: normalizeAudioVoiceForConfig(config, config.audioVoice),
audioFormat: normalizeAudioFormatForConfig(config, config.audioFormat),
audioSpeed: config.audioSpeed,
audioLanguage: config.audioLanguage,
audioDialect: config.audioDialect,
audioInstructions: config.audioInstructions,
systemPrompt: config.systemPrompt,
};
@@ -462,9 +453,11 @@ function workflowProviderConfig(config: AiConfig, requestConfig: ReturnType<type
videoGenerateAudio: config.videoGenerateAudio,
videoWatermark: config.videoWatermark,
videoArkPrivateAssetUpload: config.videoArkPrivateAssetUpload,
audioVoice: config.audioVoice,
audioFormat: config.audioFormat,
audioVoice: normalizeAudioVoiceForConfig(config, config.audioVoice),
audioFormat: normalizeAudioFormatForConfig(config, config.audioFormat),
audioSpeed: config.audioSpeed,
audioLanguage: config.audioLanguage,
audioDialect: config.audioDialect,
audioInstructions: config.audioInstructions,
workflowId: workflow.workflowId,
webappId: workflow.webappId,
@@ -497,7 +490,7 @@ function logicalCapabilityOptions(config: AiConfig, mode: BackendGenerationMode)
: mode === "video"
? { size: config.size, videoSeconds: Number(config.videoSeconds), vquality: config.vquality, videoGenerateAudio: config.videoGenerateAudio === "true", videoWatermark: config.videoWatermark === "true" }
: mode === "audio"
? { audioVoice: config.audioVoice, audioFormat: config.audioFormat, audioSpeed: Number(config.audioSpeed) }
? { audioVoice: config.audioVoice, audioFormat: config.audioFormat, audioSpeed: Number(config.audioSpeed), audioLanguage: config.audioLanguage, audioDialect: config.audioDialect }
: {};
const filtered = Object.fromEntries(Object.entries(candidates).filter(([key]) => Boolean(spec?.options?.[key])));
// 只把前台模型声明过的参数送进能力匹配。未声明的 quality 不能因为画布选了 4K 档位

View File

@@ -1,4 +1,4 @@
import { http } from "@/services/api/request";
import { compactApiParams, http } from "@/services/api/request";
import { normalizeAssetCategory, type AssetCategory } from "@/lib/asset-category";
import type { GenerationTask } from "@/services/api/task-center";
@@ -519,6 +519,49 @@ export function listVoiceProfiles() {
return http.get<{ profiles: VoiceProfile[] }>("/voice-profiles");
}
export type CharacterListPage = {
characters: ProjectCharacterDetail[];
page: number;
pageSize: number;
total: number;
hasMore: boolean;
};
export function listCharacters(options: { page?: number; pageSize?: number; query?: string; ids?: string[] } = {}) {
return http.get<CharacterListPage>("/characters", {
params: compactApiParams({
page: options.page,
pageSize: options.pageSize,
q: options.query,
ids: options.ids?.length ? options.ids.join(",") : undefined,
}),
});
}
export function getCharacter(assetId: string) {
return http.get<ProjectCharacterDetail>(`/characters/${encodeURIComponent(assetId)}`);
}
export function createCharacter(input: { name: string; definition?: Record<string, unknown>; imageResourceId?: string; audioResourceId?: string; voiceName?: string; instructions?: string }) {
return http.post<ProjectCharacterDetail>("/characters", input);
}
export function updateCharacter(assetId: string, input: { name: string; definition: Record<string, unknown> }) {
return http.patch<ProjectCharacterDetail>(`/characters/${encodeURIComponent(assetId)}`, input);
}
export function replaceCharacterRepresentations(assetId: string, representations: Array<{ role: string; resourceId: string; metadata?: Record<string, unknown> }>) {
return http.put<ProjectCharacterDetail>(`/characters/${encodeURIComponent(assetId)}/representations`, { representations });
}
export function bindCharacterVoice(assetId: string, input: { sampleResourceId?: string; voiceName?: string; instructions?: string }) {
return http.put<ProjectCharacterDetail>(`/characters/${encodeURIComponent(assetId)}/voice`, input);
}
export function unbindCharacterVoice(assetId: string) {
return http.delete<ProjectCharacterDetail>(`/characters/${encodeURIComponent(assetId)}/voice`);
}
export function createProjectCharacter(projectId: string, input: { name: string; definition?: Record<string, unknown> }) {
return http.post<ProjectCharacterDetail>(`/projects/${encodeURIComponent(projectId)}/characters`, input);
}