feat: add aspect ratio and resolution selection for scene fusion
This commit is contained in:
parent
153dae3907
commit
8643531e5d
@ -18,6 +18,8 @@ import sharp from "sharp";
|
||||
const sceneFusionRequestSchema = z.object({
|
||||
productImage: z.string().min(1, "产品图不能为空"),
|
||||
productName: z.string().optional(), // 产品名称(如"沙发")
|
||||
aspectRatio: z.string().optional(), // 图片比例(如"1:1", "4:3", "auto")
|
||||
imageSize: z.string().optional(), // 图片分辨率(如"1K", "2K", "4K")
|
||||
scenes: z.array(z.object({
|
||||
id: z.string(),
|
||||
image: z.string().min(1, "场景图不能为空"),
|
||||
@ -144,6 +146,54 @@ async function downloadAndUploadToS3(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 aspectRatio 和 imageSize 计算实际尺寸
|
||||
* @param aspectRatio - 宽高比,如 "1:1", "4:3", "16:9", "auto"
|
||||
* @param imageSize - 分辨率等级,如 "1K", "2K", "4K"
|
||||
* @returns 尺寸字符串,如 "1024*1024",或 undefined(auto 模式)
|
||||
*/
|
||||
function calculateSize(aspectRatio: string, imageSize: string): string | undefined {
|
||||
// auto 模式:不设置尺寸,让 API 根据输入图片自动决定
|
||||
if (aspectRatio === "auto") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 根据分辨率等级确定基准像素
|
||||
const baseSizeMap: Record<string, number> = {
|
||||
"1K": 1024,
|
||||
"2K": 1536,
|
||||
"4K": 2048,
|
||||
};
|
||||
const baseSize = baseSizeMap[imageSize] || 1536;
|
||||
|
||||
// 根据比例计算宽高
|
||||
const ratioMap: Record<string, [number, number]> = {
|
||||
"1:1": [1, 1],
|
||||
"4:3": [4, 3],
|
||||
"3:4": [3, 4],
|
||||
"16:9": [16, 9],
|
||||
"9:16": [9, 16],
|
||||
};
|
||||
|
||||
const ratio = ratioMap[aspectRatio];
|
||||
if (!ratio) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [w, h] = ratio;
|
||||
// 保持总像素接近 baseSize * baseSize,同时维持比例
|
||||
const scale = Math.sqrt((baseSize * baseSize) / (w * h));
|
||||
let width = Math.round(w * scale);
|
||||
let height = Math.round(h * scale);
|
||||
|
||||
// 确保宽高在 512-2048 范围内
|
||||
const clamp = (v: number) => Math.max(512, Math.min(2048, v));
|
||||
width = clamp(width);
|
||||
height = clamp(height);
|
||||
|
||||
return `${width}*${height}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用阿里生图 API
|
||||
*/
|
||||
@ -153,7 +203,8 @@ async function callAlibabaImageAPI(
|
||||
modelId: string,
|
||||
productImageUrl: string,
|
||||
sceneImageUrl: string,
|
||||
prompt: string
|
||||
prompt: string,
|
||||
size?: string
|
||||
): Promise<string> {
|
||||
// 构建消息内容:产品图 + 场景图 + 提示词
|
||||
const content = [
|
||||
@ -174,6 +225,7 @@ async function callAlibabaImageAPI(
|
||||
},
|
||||
parameters: {
|
||||
n: 1,
|
||||
...(size && { size }),
|
||||
prompt_extend: true,
|
||||
watermark: false,
|
||||
},
|
||||
@ -183,6 +235,7 @@ async function callAlibabaImageAPI(
|
||||
- Index: Scene ${sceneImageUrl.substring(sceneImageUrl.length-15)}
|
||||
- Product URL: ${productImageUrl}
|
||||
- Scene URL: ${sceneImageUrl}
|
||||
- Size: ${size || 'auto'}
|
||||
- Prompt: ${prompt}
|
||||
`);
|
||||
|
||||
@ -267,7 +320,8 @@ async function processScene(
|
||||
defaultPrompt: string,
|
||||
baseURL: string,
|
||||
apiKey: string,
|
||||
modelId: string
|
||||
modelId: string,
|
||||
size?: string
|
||||
): Promise<{ thumbnail: string; original: string; prompt: string }> {
|
||||
// 构建智能提示词
|
||||
const prompt = buildPrompt(defaultPrompt, productName, targetName, customPrompt);
|
||||
@ -281,7 +335,8 @@ async function processScene(
|
||||
modelId,
|
||||
productImageUrl,
|
||||
sceneImageUrl,
|
||||
prompt
|
||||
prompt,
|
||||
size
|
||||
);
|
||||
|
||||
// 下载并上传到 S3
|
||||
@ -322,7 +377,11 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const { productImage, productName, scenes } = parseResult.data;
|
||||
const { productImage, productName, aspectRatio, imageSize, scenes } = parseResult.data;
|
||||
|
||||
// 计算输出尺寸
|
||||
const size = calculateSize(aspectRatio || "auto", imageSize || "2K");
|
||||
console.log(`[SceneFusion] Output settings: aspectRatio=${aspectRatio || 'auto'}, imageSize=${imageSize || '2K'}, calculated size=${size || 'auto'}`);
|
||||
|
||||
// 检查场景数量限制
|
||||
if (scenes.length > config.maxScenes) {
|
||||
@ -400,9 +459,13 @@ export async function POST(request: Request) {
|
||||
config.defaultPrompt,
|
||||
provider.baseUrl,
|
||||
provider.apiKey,
|
||||
model.modelId
|
||||
model.modelId,
|
||||
size
|
||||
);
|
||||
|
||||
// 将分辨率等级用于数据库存储
|
||||
const dbImageSize = imageSize || "2K";
|
||||
|
||||
// 保存到数据库(chatId 和 messageId 设为 null,因为场景裂变不关联聊天)
|
||||
try {
|
||||
await db.insert(generatedImage).values({
|
||||
@ -411,7 +474,7 @@ export async function POST(request: Request) {
|
||||
messageId: null, // 场景裂变不关联消息
|
||||
thumbnailUrl: result.thumbnail,
|
||||
originalUrl: result.original,
|
||||
imageSize: "2K",
|
||||
imageSize: dbImageSize,
|
||||
prompt: `[场景裂变] ${result.prompt}`,
|
||||
modelId: model.modelId,
|
||||
});
|
||||
|
||||
@ -55,11 +55,30 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
const defaultPrompt = config?.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。";
|
||||
const maxScenes = config?.maxScenes || 10;
|
||||
|
||||
// 比例选项
|
||||
const aspectRatioOptions = [
|
||||
{ label: "自动", value: "auto" },
|
||||
{ label: "1:1", value: "1:1" },
|
||||
{ label: "4:3", value: "4:3" },
|
||||
{ label: "3:4", value: "3:4" },
|
||||
{ label: "16:9", value: "16:9" },
|
||||
{ label: "9:16", value: "9:16" },
|
||||
];
|
||||
|
||||
// 分辨率选项
|
||||
const imageSizeOptions = [
|
||||
{ label: "标准 (1K)", value: "1K" },
|
||||
{ label: "高清 (2K)", value: "2K" },
|
||||
{ label: "超清 (4K)", value: "4K" },
|
||||
];
|
||||
|
||||
// 状态管理
|
||||
const [productImage, setProductImage] = useState<string | null>(null);
|
||||
const [productName, setProductName] = useState<string>(""); // 产品名称(如"沙发")
|
||||
const [scenes, setScenes] = useState<SceneItem[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [aspectRatio, setAspectRatio] = useState<string>("auto"); // 图片比例
|
||||
const [imageSize, setImageSize] = useState<string>("2K"); // 图片分辨率
|
||||
|
||||
// 文件上传 Input 引用
|
||||
const productInputRef = useRef<HTMLInputElement>(null);
|
||||
@ -172,6 +191,8 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
body: JSON.stringify({
|
||||
productImage,
|
||||
productName: productName || undefined, // 产品名称
|
||||
aspectRatio, // 图片比例
|
||||
imageSize, // 图片分辨率
|
||||
scenes: scenesToProcess.map(s => ({
|
||||
id: s.id,
|
||||
image: s.image,
|
||||
@ -248,6 +269,8 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
body: JSON.stringify({
|
||||
productImage,
|
||||
productName: productName || undefined,
|
||||
aspectRatio, // 图片比例
|
||||
imageSize, // 图片分辨率
|
||||
scenes: [{
|
||||
id: scene.id,
|
||||
image: scene.image,
|
||||
@ -400,6 +423,55 @@ export function SceneFusionView({ onBack }: SceneFusionViewProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 1.5 图片尺寸设置 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-medium">输出设置</Label>
|
||||
|
||||
{/* 比例选择 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">图片比例</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{aspectRatioOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setAspectRatio(option.value)}
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-xs rounded-md border transition-colors",
|
||||
aspectRatio === option.value
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-muted border-input"
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分辨率选择 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">输出分辨率</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{imageSizeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setImageSize(option.value)}
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
"px-2.5 py-1 text-xs rounded-md border transition-colors",
|
||||
imageSize === option.value
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background hover:bg-muted border-input"
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. 场景图列表 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user