chore: image edit api
This commit is contained in:
@@ -45,6 +45,7 @@ const AutoImage: React.FC<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { ratio } = await getImgRatio(props.src || '');
|
const { ratio } = await getImgRatio(props.src || '');
|
||||||
|
console.log('ratio', ratio);
|
||||||
if (typeof height === 'number') {
|
if (typeof height === 'number') {
|
||||||
setWidth(height * ratio);
|
setWidth(height * ratio);
|
||||||
} else {
|
} else {
|
||||||
@@ -66,6 +67,7 @@ const AutoImage: React.FC<
|
|||||||
|
|
||||||
const handleImgLoad = useCallback(() => {
|
const handleImgLoad = useCallback(() => {
|
||||||
props.onLoad?.();
|
props.onLoad?.();
|
||||||
|
setIsError(false);
|
||||||
}, [props.onLoad]);
|
}, [props.onLoad]);
|
||||||
|
|
||||||
const handleOnError = () => {
|
const handleOnError = () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
|||||||
// import './iconfont/iconfont.js';
|
// import './iconfont/iconfont.js';
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_hdf3t88r4j.js'
|
scriptUrl: '//at.alicdn.com/t/c/font_4613488_bslq45omhet.js'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default IconFont;
|
export default IconFont;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type Stroke = Point[];
|
|||||||
type CanvasImageEditorProps = {
|
type CanvasImageEditorProps = {
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onSave: (imageData: string) => void;
|
onSave: (imageData: { mask: string; img: string }) => void;
|
||||||
uploadButton: React.ReactNode;
|
uploadButton: React.ReactNode;
|
||||||
imageStatus: {
|
imageStatus: {
|
||||||
isOriginal: boolean;
|
isOriginal: boolean;
|
||||||
@@ -131,20 +131,44 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
|
|||||||
const maskCanvas = document.createElement('canvas');
|
const maskCanvas = document.createElement('canvas');
|
||||||
maskCanvas.width = overlayCanvas.width;
|
maskCanvas.width = overlayCanvas.width;
|
||||||
maskCanvas.height = overlayCanvas.height;
|
maskCanvas.height = overlayCanvas.height;
|
||||||
const maskCtx = maskCanvas.getContext('2d');
|
const maskCtx = maskCanvas.getContext('2d')!;
|
||||||
|
const overlayCtx = overlayCanvas.getContext('2d')!;
|
||||||
|
|
||||||
// Create the transparent overlay
|
const imageData = overlayCtx.getImageData(
|
||||||
maskCtx!.fillStyle = 'black';
|
0,
|
||||||
maskCtx!.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
|
0,
|
||||||
maskCtx!.globalCompositeOperation = 'destination-out';
|
overlayCanvas.width,
|
||||||
maskCtx!.drawImage(overlayCanvas, 0, 0);
|
overlayCanvas.height
|
||||||
|
);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const alpha = data[i + 3];
|
||||||
|
if (alpha > 0) {
|
||||||
|
data[i] = 255; // Red
|
||||||
|
data[i + 1] = 255; // Green
|
||||||
|
data[i + 2] = 255; // Blue
|
||||||
|
data[i + 3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maskCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
maskCtx.globalCompositeOperation = 'destination-over';
|
||||||
|
maskCtx.fillStyle = 'black';
|
||||||
|
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
|
||||||
return maskCanvas.toDataURL('image/png');
|
return maskCanvas.toDataURL('image/png');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const saveMask = useCallback(() => {
|
const generateImage = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current!;
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveImage = useCallback(() => {
|
||||||
const mask = generateMask();
|
const mask = generateMask();
|
||||||
onSave(mask);
|
const img = generateImage();
|
||||||
|
onSave({ mask, img });
|
||||||
}, [onSave, generateMask]);
|
}, [onSave, generateMask]);
|
||||||
|
|
||||||
const downloadMask = useCallback(() => {
|
const downloadMask = useCallback(() => {
|
||||||
@@ -273,7 +297,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
|
|||||||
|
|
||||||
currentStroke.current = [];
|
currentStroke.current = [];
|
||||||
|
|
||||||
saveMask();
|
saveImage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearOverlayCanvas = useCallback(() => {
|
const clearOverlayCanvas = useCallback(() => {
|
||||||
@@ -560,7 +584,7 @@ const CanvasImageEditor: React.FC<CanvasImageEditorProps> = ({
|
|||||||
<div className="tools">
|
<div className="tools">
|
||||||
<Tooltip title="Save Mask">
|
<Tooltip title="Save Mask">
|
||||||
<Button onClick={downloadMask} size="middle" type="text">
|
<Button onClick={downloadMask} size="middle" type="text">
|
||||||
<IconFont className="font-size-14" type="icon-save1"></IconFont>
|
<IconFont className="font-size-14" type="icon-save2"></IconFont>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Download">
|
<Tooltip title="Download">
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
|||||||
></CopyButton>
|
></CopyButton>
|
||||||
{actions?.map((item) => {
|
{actions?.map((item) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div key={item.key}>
|
||||||
<Divider
|
<Divider
|
||||||
style={{
|
style={{
|
||||||
marginBlock: 5,
|
marginBlock: 5,
|
||||||
@@ -99,7 +99,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
|
|||||||
<span className="font-size-14">{item.icon}</span>
|
<span className="font-size-14">{item.icon}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const advancedFieldsDefaultValus = {
|
|||||||
seed: null,
|
seed: null,
|
||||||
sample_method: 'euler_a',
|
sample_method: 'euler_a',
|
||||||
cfg_scale: 4.5,
|
cfg_scale: 4.5,
|
||||||
|
guidance: 3.5,
|
||||||
sampling_steps: 10,
|
sampling_steps: 10,
|
||||||
negative_prompt: null,
|
negative_prompt: null,
|
||||||
schedule_method: 'discrete',
|
schedule_method: 'discrete',
|
||||||
@@ -404,6 +405,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
}
|
}
|
||||||
const formValues = form.current?.form?.getFieldsValue();
|
const formValues = form.current?.form?.getFieldsValue();
|
||||||
return ImageAdvancedParamsConfig.map((item: ParamsSchema) => {
|
return ImageAdvancedParamsConfig.map((item: ParamsSchema) => {
|
||||||
|
if (item.name === 'strength') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={item.name}
|
name={item.name}
|
||||||
@@ -418,6 +422,11 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
? item.disabledConfig?.when?.(formValues)
|
? item.disabledConfig?.when?.(formValues)
|
||||||
: item.disabled
|
: item.disabled
|
||||||
}
|
}
|
||||||
|
description={
|
||||||
|
item.description?.isLocalized
|
||||||
|
? intl.formatMessage({ id: item.description.text })
|
||||||
|
: item.description?.text
|
||||||
|
}
|
||||||
onChange={item.name === 'random_seed' ? handleFieldChange : null}
|
onChange={item.name === 'random_seed' ? handleFieldChange : null}
|
||||||
{..._.omit(item, ['name', 'rules', 'disabledConfig'])}
|
{..._.omit(item, ['name', 'rules', 'disabledConfig'])}
|
||||||
></FieldComponent>
|
></FieldComponent>
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const advancedFieldsDefaultValus = {
|
|||||||
seed: 1,
|
seed: 1,
|
||||||
sample_method: 'euler_a',
|
sample_method: 'euler_a',
|
||||||
cfg_scale: 4.5,
|
cfg_scale: 4.5,
|
||||||
|
guidance: 3.5,
|
||||||
|
strength: 0.75,
|
||||||
sampling_steps: 10,
|
sampling_steps: 10,
|
||||||
negative_prompt: null,
|
negative_prompt: null,
|
||||||
preview: null,
|
preview: null,
|
||||||
@@ -152,10 +154,12 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
}, [parameters.n]);
|
}, [parameters.n]);
|
||||||
|
|
||||||
const imageFile = useMemo(() => {
|
const imageFile = useMemo(() => {
|
||||||
|
console.log('image:', image);
|
||||||
return base64ToFile(image, 'image');
|
return base64ToFile(image, 'image');
|
||||||
}, [image]);
|
}, [image]);
|
||||||
|
|
||||||
const maskFile = useMemo(() => {
|
const maskFile = useMemo(() => {
|
||||||
|
console.log('mask:', mask);
|
||||||
return base64ToFile(mask, 'mask');
|
return base64ToFile(mask, 'mask');
|
||||||
}, [mask]);
|
}, [mask]);
|
||||||
|
|
||||||
@@ -222,7 +226,9 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
setCurrentPrompt(current?.content || '');
|
setCurrentPrompt(current?.content || '');
|
||||||
setRouteCache(routeCachekey.playgroundTextToImage, true);
|
setRouteCache(routeCachekey.playgroundTextToImage, true);
|
||||||
|
|
||||||
const imgSize = _.split(finalParameters.size, 'x');
|
const imgSize = _.split(finalParameters.size, 'x').map((item: string) =>
|
||||||
|
_.toNumber(item)
|
||||||
|
);
|
||||||
|
|
||||||
// preview
|
// preview
|
||||||
let stream_options: Record<string, any> = {
|
let stream_options: Record<string, any> = {
|
||||||
@@ -278,9 +284,12 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
|
|
||||||
const result: any = await fetchChunkedData({
|
const result: any = await fetchChunkedData({
|
||||||
data: params,
|
data: params,
|
||||||
url: `${EDIT_IMAGE_API}?t=${Date.now()}`,
|
url: `http://192.168.50.174:40935/v1/images/edits?t=${Date.now()}`,
|
||||||
|
// url: EDIT_IMAGE_API,
|
||||||
signal: requestToken.current.signal
|
signal: requestToken.current.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('result:', result);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
setTokenResult({
|
setTokenResult({
|
||||||
error: true,
|
error: true,
|
||||||
@@ -432,6 +441,11 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
? item.disabledConfig?.when?.(formValues)
|
? item.disabledConfig?.when?.(formValues)
|
||||||
: item.disabled
|
: item.disabled
|
||||||
}
|
}
|
||||||
|
description={
|
||||||
|
item.description?.isLocalized
|
||||||
|
? intl.formatMessage({ id: item.description.text })
|
||||||
|
: item.description?.text || ''
|
||||||
|
}
|
||||||
onChange={item.name === 'random_seed' ? handleFieldChange : null}
|
onChange={item.name === 'random_seed' ? handleFieldChange : null}
|
||||||
{..._.omit(item, ['name', 'rules', 'disabledConfig'])}
|
{..._.omit(item, ['name', 'rules', 'disabledConfig'])}
|
||||||
></FieldComponent>
|
></FieldComponent>
|
||||||
@@ -494,16 +508,16 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
setImageList([]);
|
setImageList([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOnSave = useCallback((url: string) => {
|
const handleOnSave = useCallback((data: { img: string; mask: string }) => {
|
||||||
setImageStatus({
|
setImageStatus({
|
||||||
isOriginal: true,
|
isOriginal: true,
|
||||||
isResetNeeded: false
|
isResetNeeded: false
|
||||||
});
|
});
|
||||||
setMask(url);
|
setMask(data.mask);
|
||||||
|
setImage(data.img);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderImageEditor = useMemo(() => {
|
const renderImageEditor = useMemo(() => {
|
||||||
console.log('image:', image);
|
|
||||||
if (image) {
|
if (image) {
|
||||||
return (
|
return (
|
||||||
<CanvasImageEditor
|
<CanvasImageEditor
|
||||||
@@ -517,6 +531,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
handleUpdateImgList={handleUpdateImageList}
|
handleUpdateImgList={handleUpdateImageList}
|
||||||
size="middle"
|
size="middle"
|
||||||
|
accept="image/png"
|
||||||
></UploadImg>
|
></UploadImg>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
@@ -525,6 +540,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<UploadImg
|
<UploadImg
|
||||||
|
accept="image/png"
|
||||||
drag={true}
|
drag={true}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
handleUpdateImgList={handleUpdateImageList}
|
handleUpdateImgList={handleUpdateImageList}
|
||||||
@@ -676,7 +692,6 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 125,
|
height: 125,
|
||||||
minWidth: 125,
|
|
||||||
maxHeight: 125
|
maxHeight: 125
|
||||||
}}
|
}}
|
||||||
key={item.uid}
|
key={item.uid}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface UploadImgProps {
|
|||||||
drag?: boolean;
|
drag?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
accept?: string;
|
||||||
handleUpdateImgList: (
|
handleUpdateImgList: (
|
||||||
imgList: { dataUrl: string; uid: number | string }[]
|
imgList: { dataUrl: string; uid: number | string }[]
|
||||||
) => void;
|
) => void;
|
||||||
@@ -24,6 +25,7 @@ const UploadImg: React.FC<UploadImgProps> = ({
|
|||||||
drag = false,
|
drag = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
children,
|
children,
|
||||||
|
accept = 'image/*',
|
||||||
size = 'small'
|
size = 'small'
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -81,7 +83,7 @@ const UploadImg: React.FC<UploadImgProps> = ({
|
|||||||
{drag ? (
|
{drag ? (
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
ref={uploadRef}
|
ref={uploadRef}
|
||||||
accept="image/*"
|
accept={accept}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
action="/"
|
action="/"
|
||||||
fileList={[]}
|
fileList={[]}
|
||||||
@@ -104,7 +106,7 @@ const UploadImg: React.FC<UploadImgProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<Upload
|
<Upload
|
||||||
ref={uploadRef}
|
ref={uploadRef}
|
||||||
accept="image/*"
|
accept={accept}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
action="/"
|
action="/"
|
||||||
fileList={[]}
|
fileList={[]}
|
||||||
|
|||||||
@@ -297,6 +297,47 @@ export const ImageAdvancedParamsConfig: ParamsSchema[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'InputNumber',
|
||||||
|
name: 'guidance',
|
||||||
|
label: {
|
||||||
|
text: 'Guidance',
|
||||||
|
isLocalized: false
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
min: 1.0,
|
||||||
|
max: 10,
|
||||||
|
step: 0.1
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'InputNumber',
|
||||||
|
name: 'strength',
|
||||||
|
label: {
|
||||||
|
text: 'Strength',
|
||||||
|
isLocalized: false
|
||||||
|
},
|
||||||
|
// description: {
|
||||||
|
// text: '值越高,它对原图的修改越大,更多变化',
|
||||||
|
// html: false,
|
||||||
|
// isLocalized: false
|
||||||
|
// },
|
||||||
|
attrs: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
step: 0.1
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'InputNumber',
|
type: 'InputNumber',
|
||||||
name: 'cfg_scale',
|
name: 'cfg_scale',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import useWindowResize from '@/hooks/use-window-resize';
|
|||||||
import { DiffOutlined, HighlightOutlined } from '@ant-design/icons';
|
import { DiffOutlined, HighlightOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Space, Tabs, TabsProps } from 'antd';
|
import { Button, Segmented, Space, Tabs, TabsProps } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -155,14 +155,14 @@ const TextToImages: React.FC = () => {
|
|||||||
<span className="font-600">
|
<span className="font-600">
|
||||||
{intl.formatMessage({ id: 'menu.playground.text2images' })}
|
{intl.formatMessage({ id: 'menu.playground.text2images' })}
|
||||||
</span>
|
</span>
|
||||||
{/* {
|
{
|
||||||
<Segmented
|
<Segmented
|
||||||
options={optionsList}
|
options={optionsList}
|
||||||
size="middle"
|
size="middle"
|
||||||
className="m-l-40"
|
className="m-l-40"
|
||||||
onChange={(key) => setActiveKey(key)}
|
onChange={(key) => setActiveKey(key)}
|
||||||
></Segmented>
|
></Segmented>
|
||||||
} */}
|
}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
breadcrumb: {}
|
breadcrumb: {}
|
||||||
|
|||||||
@@ -72,13 +72,10 @@ const createFormData = (data: any): FormData => {
|
|||||||
|
|
||||||
const appendToFormData = (key: string, value: any) => {
|
const appendToFormData = (key: string, value: any) => {
|
||||||
if (value instanceof File) {
|
if (value instanceof File) {
|
||||||
// 处理文件类型
|
|
||||||
formData.append(key, value);
|
formData.append(key, value);
|
||||||
} else if (typeof value === 'object' && value !== null) {
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
// 如果是对象或数组,序列化为 JSON 字符串
|
|
||||||
formData.append(key, JSON.stringify(value));
|
formData.append(key, JSON.stringify(value));
|
||||||
} else {
|
} else {
|
||||||
// 处理基本数据类型
|
|
||||||
formData.append(key, String(value));
|
formData.append(key, String(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -106,7 +103,6 @@ export const fetchChunkedDataPostFormData = async (params: {
|
|||||||
body: createFormData(params.data),
|
body: createFormData(params.data),
|
||||||
signal: params.signal
|
signal: params.signal
|
||||||
});
|
});
|
||||||
console.log('response====', response);
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
@@ -170,6 +166,7 @@ export const readLargeStreamData = async (
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader?.read?.();
|
const { done, value } = await reader?.read?.();
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
// Process remaining buffered data
|
// Process remaining buffered data
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
@@ -198,6 +195,12 @@ export const readLargeStreamData = async (
|
|||||||
console.error('Failed to parse JSON:', jsonStr, e);
|
console.error('Failed to parse JSON:', jsonStr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('error:')) {
|
||||||
|
const errorStr = line.slice(7).trim();
|
||||||
|
const jsonData = JSON.parse(errorStr);
|
||||||
|
callback({ error: jsonData });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user