chore: image edit api

This commit is contained in:
jialin
2024-12-30 08:37:58 +08:00
parent cb98812444
commit 0b423a6fe0
10 changed files with 125 additions and 29 deletions
+2
View File
@@ -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 = () => {
+1 -1
View File
@@ -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;
+35 -11
View File
@@ -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">
+2 -2
View File
@@ -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>
+21 -6
View File
@@ -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',
+3 -3
View File
@@ -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: {}
+7 -4
View File
@@ -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 });
}
} }
} }
}; };