Commit 232de8ef authored by yaoke.yk's avatar yaoke.yk

客户端设定-道具修改

parent 347882ac
This diff is collapsed.
......@@ -27,6 +27,31 @@ export function useToggleFavorite(type: string, library: string) {
});
}
export function useUpdateAsset(type: string, library: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, patch }: { id: string; patch: Partial<CreateAssetPayload> }) =>
assetsApi.update(id, patch),
onSuccess: () => qc.invalidateQueries({ queryKey: assetsKey(type, library) }),
});
}
export function useUploadAssetImage(type: string, library: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, file }: { id: string; file: File }) => assetsApi.uploadImage(id, file),
onSuccess: () => qc.invalidateQueries({ queryKey: assetsKey(type, library) }),
});
}
export function useGenerateAssetImage(type: string, library: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => assetsApi.generateImage(id),
onSuccess: () => qc.invalidateQueries({ queryKey: assetsKey(type, library) }),
});
}
export function useDeleteAsset(type: string, library: string) {
const qc = useQueryClient();
return useMutation({
......
......@@ -42,6 +42,21 @@ export const assetsApi = {
const r = await apiClient.put(`/assets/${id}`, patch);
return r.data.data;
},
uploadImage: async (id: string, file: File): Promise<GlobalAsset> => {
const form = new FormData();
form.append("file", file);
const r = await apiClient.post(`/assets/${id}/upload-image`, form, {
headers: { "Content-Type": "multipart/form-data" },
timeout: 600_000,
});
return r.data.data;
},
generateImage: async (id: string): Promise<GlobalAsset> => {
const r = await apiClient.post(`/assets/${id}/generate-image`, undefined, {
timeout: 180_000,
});
return r.data.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/assets/${id}`);
},
......
......@@ -7,12 +7,17 @@ import com.yaoai.common.exception.ErrorCode;
import com.yaoai.common.response.ApiResponse;
import com.yaoai.domain.entity.GlobalAsset;
import com.yaoai.domain.mapper.GlobalAssetMapper;
import com.yaoai.pipeline.service.ImageGenPipelineService;
import com.yaoai.security.context.TenantContext;
import com.yaoai.storage.service.TosService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
......@@ -23,6 +28,8 @@ import java.util.Map;
public class GlobalAssetsController {
private final GlobalAssetMapper globalAssetMapper;
private final TosService tosService;
private final ImageGenPipelineService imageGenPipelineService;
@Operation(summary = "获取资产列表")
@GetMapping
......@@ -54,7 +61,11 @@ public class GlobalAssetsController {
asset.setDescription((String) body.get("description"));
asset.setIsFavorite(0);
Object srcProject = body.get("sourceProjectId");
if (srcProject instanceof Number) asset.setSourceProjectId(((Number) srcProject).longValue());
if (srcProject instanceof Number) {
asset.setSourceProjectId(((Number) srcProject).longValue());
} else if (srcProject instanceof String srcProjectText && !srcProjectText.isBlank()) {
asset.setSourceProjectId(Long.valueOf(srcProjectText));
}
globalAssetMapper.insert(asset);
return ApiResponse.success(GlobalAssetDTO.from(asset));
......@@ -90,6 +101,65 @@ public class GlobalAssetsController {
return ApiResponse.success(GlobalAssetDTO.from(asset));
}
@Operation(summary = "AI 生成资产图片")
@PostMapping("/{id}/generate-image")
public ApiResponse<GlobalAssetDTO> generateImage(@PathVariable Long id) {
Long tenantId = TenantContext.get();
Long userId = StpUtil.getLoginIdAsLong();
GlobalAsset asset = globalAssetMapper.selectById(id);
if (asset == null || !tenantId.equals(asset.getTenantId())) {
throw new BizException(ErrorCode.NOT_FOUND, "资产不存在");
}
if (!"prop".equals(asset.getAssetType())) {
throw new BizException(ErrorCode.INVALID_PARAM, "当前仅支持生成道具图片");
}
Long projectKeyId = asset.getSourceProjectId() != null ? asset.getSourceProjectId() : 0L;
String key = imageGenPipelineService.generateAndStore(
tenantId,
userId,
projectKeyId,
"global-assets/" + asset.getAssetType(),
buildPropImagePrompt(asset)
);
asset.setImageTosKey(key);
asset.setImageUrl(tosService.publicUrl(key));
globalAssetMapper.updateById(asset);
return ApiResponse.success(GlobalAssetDTO.from(asset));
}
@Operation(summary = "上传资产图片")
@PostMapping(value = "/{id}/upload-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<GlobalAssetDTO> uploadImage(@PathVariable Long id,
@RequestPart("file") MultipartFile file) throws IOException {
Long tenantId = TenantContext.get();
GlobalAsset asset = globalAssetMapper.selectById(id);
if (asset == null || !tenantId.equals(asset.getTenantId())) {
throw new BizException(ErrorCode.NOT_FOUND, "资产不存在");
}
if (file == null || file.isEmpty()) {
throw new BizException(ErrorCode.INVALID_PARAM, "图片文件不能为空");
}
String contentType = file.getContentType() != null ? file.getContentType() : "application/octet-stream";
if (!contentType.startsWith("image/")) {
throw new BizException(ErrorCode.INVALID_PARAM, "请上传图片文件");
}
Long projectKeyId = asset.getSourceProjectId() != null ? asset.getSourceProjectId() : 0L;
String key = TosService.buildKey(
tenantId,
projectKeyId,
"global-assets/" + asset.getAssetType(),
file.getOriginalFilename()
);
tosService.upload(key, file.getInputStream(), file.getSize(), contentType);
asset.setImageTosKey(key);
asset.setImageUrl(tosService.publicUrl(key));
globalAssetMapper.updateById(asset);
return ApiResponse.success(GlobalAssetDTO.from(asset));
}
@Operation(summary = "删除资产")
@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable Long id) {
......@@ -101,4 +171,20 @@ public class GlobalAssetsController {
globalAssetMapper.deleteById(id);
return ApiResponse.success();
}
private String buildPropImagePrompt(GlobalAsset asset) {
String category = asset.getTags() != null && !asset.getTags().isBlank() ? asset.getTags() : "道具";
String description = asset.getDescription() != null && !asset.getDescription().isBlank()
? asset.getDescription()
: "根据名称和分类设计清晰可识别的影视道具";
return """
Cinematic AI short-drama prop reference image, single object only.
Create one clear hero reference image for a prop used in video generation.
Neutral clean studio background, three-quarter view, accurate material, scale cues, sharp silhouette, realistic lighting.
No people, no hands, no environment clutter, no text, no labels, no watermark, no logo, no duplicated objects.
Prop name: %s
Prop category: %s
Prop description: %s
""".formatted(asset.getName(), category, description);
}
}
......@@ -13,4 +13,6 @@ public interface ImageGenPipelineService {
String generateAndStore(Long tenantId, Long projectId, String prompt);
String generateAndStore(Long tenantId, Long userId, Long projectId, String prompt);
String generateAndStore(Long tenantId, Long userId, Long projectId, String assetType, String prompt);
}
......@@ -39,6 +39,11 @@ public class ImageGenPipelineServiceImpl implements ImageGenPipelineService {
@Override
public String generateAndStore(Long tenantId, Long userId, Long projectId, String prompt) {
return generateAndStore(tenantId, userId, projectId, "scene", prompt);
}
@Override
public String generateAndStore(Long tenantId, Long userId, Long projectId, String assetType, String prompt) {
billingService.checkBalance(BillingChargeRequest.builder()
.tenantId(tenantId)
.userId(userId)
......@@ -60,9 +65,10 @@ public class ImageGenPipelineServiceImpl implements ImageGenPipelineService {
byte[] imageBytes = downloadImage(imageUrl);
// 3. 上传到 TOS
String key = TosService.buildKey(tenantId, projectId, "scene", "scene.jpg");
String normalizedAssetType = assetType != null && !assetType.isBlank() ? assetType : "image";
String key = TosService.buildKey(tenantId, projectId, normalizedAssetType, normalizedAssetType + ".jpg");
tosService.upload(key, new ByteArrayInputStream(imageBytes), imageBytes.length, "image/jpeg");
log.info("Scene image stored: key={}", key);
log.info("Generated image stored: key={}", key);
billingService.charge(BillingChargeRequest.builder()
.tenantId(tenantId)
.userId(userId)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment