feat: playground video

This commit is contained in:
jialin
2026-01-12 20:30:24 +08:00
parent cd3b0607a5
commit 89fe8e68f4
21 changed files with 1143 additions and 13 deletions
+10
View File
@@ -73,6 +73,16 @@ export default [
selectedIcon: 'icon-audio-filled',
defaultIcon: 'icon-audio1',
component: './playground/speech'
},
{
name: 'video',
title: 'Video',
path: '/playground/video',
key: 'video',
icon: 'icon-video',
selectedIcon: 'icon-video-filled',
defaultIcon: 'icon-video',
component: './playground/video'
}
]
},
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_rajpu0blzkp.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_h1excejg8p8.js'
});
export default IconFont;
+2 -1
View File
@@ -45,7 +45,8 @@ const NO_CONTAINER_PAGES = [
'image',
'text2images',
'clusterDetail',
'clusterCreate'
'clusterCreate',
'video'
];
const CHECK_RESOURCE_PATH = [
+1
View File
@@ -6,6 +6,7 @@ export default {
'menu.playground.chat': 'Chat',
'menu.playground.speech': 'Audio',
'menu.playground.text2images': 'Image',
'menu.playground.video': 'Video',
'menu.compare': 'Compare',
'menu.models': 'Models',
'menu.models.modelList': 'Deploy & Manage',
+2 -1
View File
@@ -62,7 +62,8 @@ export default {
'playground.params.width': 'Width',
'playground.params.height': 'Height',
'playground.params.custom': 'Custom',
'playground.params.empty.tips': 'Generated images will appear here',
'playground.params.empty.tips': 'Generated images will appear here.',
'playground.video.empty.tips': 'Generated video will appear here.',
'playground.params.standard': 'Standard',
'playground.params.hd': 'HD',
'playground.params.style.vivid': 'Vivid',
+1
View File
@@ -6,6 +6,7 @@ export default {
'menu.playground.chat': 'チャット',
'menu.playground.speech': '音声',
'menu.playground.text2images': '画像生成',
'menu.playground.video': '動画',
'menu.compare': '比較',
'menu.models': 'モデル',
'menu.models.modelList': 'デプロイと管理',
+2 -1
View File
@@ -62,7 +62,8 @@ export default {
'playground.params.width': '幅',
'playground.params.height': '高さ',
'playground.params.custom': 'カスタム',
'playground.params.empty.tips': '生成された画像はここに表示されます',
'playground.params.empty.tips': '生成された画像はここに表示されます',
'playground.video.empty.tips': '生成された動画はここに表示されます。',
'playground.params.standard': '標準',
'playground.params.hd': 'HD',
'playground.params.style.vivid': '鮮やか',
+1
View File
@@ -6,6 +6,7 @@ export default {
'menu.playground.chat': 'Чат',
'menu.playground.speech': 'Аудио',
'menu.playground.text2images': 'Генерация изображений',
'menu.playground.video': 'Видео',
'menu.compare': 'Сравнение',
'menu.models': 'Модели',
'menu.models.modelList': 'Развертывание и управление',
+1
View File
@@ -63,6 +63,7 @@ export default {
'playground.params.height': 'Высота',
'playground.params.custom': 'Дополнительно',
'playground.params.empty.tips': 'Здесь появятся сгенерированные изображения',
'playground.video.empty.tips': 'Здесь появятся сгенерированные видео.',
'playground.params.standard': 'Стандарт',
'playground.params.hd': 'HD',
'playground.params.style.vivid': 'Яркий',
+1
View File
@@ -6,6 +6,7 @@ export default {
'menu.playground.chat': '对话',
'menu.playground.speech': '语音',
'menu.playground.text2images': '图像',
'menu.playground.video': '视频',
'menu.compare': '多模型对比',
'menu.models': '模型',
'menu.models.modelList': '部署与管理',
+1
View File
@@ -67,6 +67,7 @@ export default {
'playground.params.style.vivid': '生动',
'playground.params.style.natural': '自然',
'playground.params.empty.tips': '生成的图片将出现在这里',
'playground.video.empty.tips': '生成的视频将出现在这里',
'playground.embedding.documents': '文档',
'playground.embedding.addtext': '添加文本',
'playground.embedding.inputyourtext': '输入你的文本',
@@ -499,11 +499,6 @@ const options: BackendParameter[] = [
value: '--enable-lora-bias',
options: []
},
{
label: '--rope-scaling',
value: '--rope-scaling',
options: []
},
{
label: '--rope-theta',
value: '--rope-theta',
+20
View File
@@ -5,6 +5,7 @@ import { request } from '@umijs/max';
export { GPUSTACK_API_BASE_URL, OPENAI_COMPATIBLE };
export const CHAT_API = `/${OPENAI_COMPATIBLE}/chat/completions`;
export const CREATE_VIDEO_API = `/${OPENAI_COMPATIBLE}/videos`;
export const CREAT_IMAGE_API = `/${OPENAI_COMPATIBLE}/images/generations`;
export const EDIT_IMAGE_API = `/${OPENAI_COMPATIBLE}/images/edits`;
@@ -128,6 +129,25 @@ export const createImage = async (params: {
return response.json();
};
export const createVideo = async (params: {
data?: any;
signal?: AbortSignal;
}) => {
const response = await fetch(CREATE_VIDEO_API, {
method: 'POST',
body: JSON.stringify(params.data),
signal: params.signal,
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
return await errorHandler(response);
}
return response.json();
};
// ============ audio ============
export const textToSpeech = async (params: any, options?: any) => {
const res = await fetch(AUDIO_TEXT_TO_SPEECH_API, {
@@ -0,0 +1,275 @@
import { setRouteCache } from '@/atoms/route-cache';
import AlertInfo from '@/components/alert-info';
import IconFont from '@/components/icon-font';
import routeCachekey from '@/config/route-cachekey';
import ThumbImg from '@/pages/playground/components/thumb-img';
import { VideoCameraOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css';
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react';
import { CREATE_VIDEO_API } from '../apis';
import { useInitVideoMeta } from '../hooks/use-init-video-meta';
import useTextVideo from '../hooks/use-text-video';
import '../style/ground-llm.less';
import '../style/system-message-wrap.less';
import { generateCode } from '../view-code/video';
import DynamicParams from './dynamic-params';
import MessageInput from './message-input';
import ViewCommonCode from './view-common-code';
interface MessageProps {
modelList: Global.BaseOption<string>[];
loaded?: boolean;
ref?: any;
}
const GroundVideo: React.FC<MessageProps> = forwardRef((props, ref) => {
const { modelList } = props;
const intl = useIntl();
const [show, setShow] = useState(false);
const [collapse, setCollapse] = useState(false);
const scroller = useRef<any>(null);
const paramsRef = useRef<any>(null);
const inputRef = useRef<any>(null);
const {
handleOnValuesChange,
handleToggleParamsStyle,
setParams,
form,
formFields,
paramsConfig,
initialValues,
parameters,
isOpenaiCompatible
} = useInitVideoMeta(props, {
type: 'create'
});
const {
loading,
tokenResult,
videoList,
promptList,
currentPrompt,
setCurrentPrompt,
handleClear,
handleStopConversation,
submitMessage
} = useTextVideo({
scroller,
paramsRef,
API: CREATE_VIDEO_API
});
useImperativeHandle(ref, () => {
return {
viewCode() {
setShow(true);
},
setCollapse() {
setCollapse(!collapse);
},
collapse: collapse
};
});
const generateNumber = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const handleRandomPrompt = useCallback(() => {
const randomIndex = generateNumber(0, promptList.length - 1);
const randomPrompt = promptList[randomIndex];
inputRef.current?.handleInputChange({
target: {
value: randomPrompt
}
});
}, []);
const finalParameters = useMemo(() => {
if (parameters.size === 'custom') {
return {
..._.omit(parameters, ['width', 'height', 'random_seed', 'seed']),
size:
parameters.width && parameters.height
? `${parameters.width}x${parameters.height}`
: ''
};
}
return {
..._.omit(parameters, ['width', 'height', 'random_seed', 'seed'])
};
}, [parameters]);
const viewCodeContent = useMemo(() => {
return generateCode({
api: CREATE_VIDEO_API,
parameters: {
...finalParameters,
prompt: currentPrompt
}
});
}, [finalParameters, isOpenaiCompatible, currentPrompt]);
const handleInputChange = (e: any) => {
setCurrentPrompt(e.target.value);
};
const generateParams = () => {
const params = {
..._.omitBy(finalParameters, (value: string) => !value),
prompt: currentPrompt
};
return params;
};
const handleSendMessage = async () => {
try {
await form.current?.form?.validateFields();
if (!parameters.model) return;
const params = generateParams();
console.log('generateParams:', params);
setParams({
...parameters
});
console.log('params:', params, parameters);
setRouteCache(routeCachekey['/playground/video'], true);
await submitMessage(params);
} catch (error) {
// console.log('error:', error);
} finally {
console.log('finally---------');
setRouteCache(routeCachekey['/playground/video'], false);
}
};
const handleCloseViewCode = useCallback(() => {
setShow(false);
}, []);
return (
<div className="ground-left-wrapper">
<div className="ground-left">
<div
className="message-list-wrap"
ref={scroller}
style={{ paddingBottom: 16 }}
>
<>
<div className="content" style={{ height: '100%' }}>
<ThumbImg
style={{
padding: 0,
height: '100%',
justifyContent: 'center',
flexDirection: 'column',
flexWrap: 'unset',
alignItems: 'center'
}}
autoBgColor={false}
editable={false}
dataList={videoList}
responseable={true}
gutter={[8, 16]}
autoSize={true}
></ThumbImg>
{!videoList.length && (
<div className="flex-column font-size-14 flex-center gap-20 justify-center hold-wrapper">
<VideoCameraOutlined className="font-size-32 text-secondary" />
<span>
{intl.formatMessage({
id: 'playground.video.empty.tips'
})}
</span>
</div>
)}
</div>
</>
</div>
{tokenResult && (
<div style={{ height: 40 }}>
<AlertInfo
type="danger"
message={tokenResult?.errorMessage}
></AlertInfo>
</div>
)}
<div className="ground-left-footer">
<MessageInput
ref={inputRef}
placeholer={intl.formatMessage({
id: 'playground.input.prompt.holder'
})}
actions={[]}
defaultSize={{
minRows: 5,
maxRows: 5
}}
title={intl.formatMessage({ id: 'playground.image.prompt' })}
loading={loading}
disabled={!parameters.model}
isEmpty={!videoList.length}
handleSubmit={handleSendMessage}
handleAbortFetch={handleStopConversation}
onInputChange={handleInputChange}
shouldResetMessage={false}
clearAll={handleClear}
tools={
<>
<Tooltip
title={intl.formatMessage({
id: 'playground.image.prompt.random'
})}
>
<Button
onClick={handleRandomPrompt}
size="middle"
type="text"
icon={<IconFont type="icon-random"></IconFont>}
></Button>
</Tooltip>
</>
}
/>
</div>
</div>
<div
className={classNames('params-wrapper', {
collapsed: collapse
})}
ref={paramsRef}
>
<div className="box">
<DynamicParams
ref={form}
onValuesChange={handleOnValuesChange}
paramsConfig={paramsConfig}
initialValues={initialValues}
modelList={modelList}
/>
</div>
</div>
<ViewCommonCode
open={show}
viewCodeContent={viewCodeContent}
onCancel={handleCloseViewCode}
title={intl.formatMessage({ id: 'playground.viewcode' })}
></ViewCommonCode>
</div>
);
});
export default GroundVideo;
@@ -0,0 +1,54 @@
import { ImageSizeItem } from './params-config';
import { ParamsSchema } from './types';
export const videoSizeOptions: ImageSizeItem[] = [
{ label: '720x1280', value: '720x1280', width: 720, height: 1280 },
{ label: '1280x720', value: '1280x720', width: 1280, height: 720 },
{ label: '1024x1792', value: '1024x1792', width: 1024, height: 1792 },
{
label: '1792x1024',
value: '1792x1024',
width: 1792,
height: 1024
}
];
export const videoParamsConfig: ParamsSchema[] = [
{
type: 'Select',
name: 'seconds',
options: [
{ label: '4', value: '4' },
{ label: '8', value: '8' },
{ label: '12', value: '12' }
],
label: {
text: 'Duration (seconds)',
isLocalized: false
},
rules: [
{
required: false
}
]
},
{
type: 'Select',
name: 'size',
options: videoSizeOptions,
description: {
text: 'playground.params.size.description',
html: true,
isLocalized: true
},
label: {
text: 'playground.params.size',
isLocalized: true
},
rules: [
{
required: false
}
]
}
];
+4
View File
@@ -40,6 +40,8 @@ export const IMG_METAKEYS = [
'strength'
];
export const VIDEO_METAKEYS = [];
export const llmInitialValues = {
seed: null,
stop: null,
@@ -62,6 +64,8 @@ export const advancedFieldsDefaultValus = {
// preview: 'preview_faster'
};
export const videoAdvancedDefaultValues = {};
export const openaiCompatibleFieldsDefaultValus = {
// quality: 'standard',
style: null
+7 -4
View File
@@ -279,10 +279,13 @@ export const useInitImageMeta = (
) => {
if (sizeOptions.length) {
const sizeConfig = ImageSizeConfig.map((item) => {
return {
...item,
options: sizeOptions
};
if (item.name === 'size') {
return {
...item,
options: sizeOptions
};
}
return item;
});
return [...ImageCountConfig, ...sizeConfig];
}
@@ -0,0 +1,348 @@
import {
ImageCustomSizeConfig,
ImageSizeItem
} from '@/pages/playground/config/params-config';
import { generateRandomNumber } from '@/utils';
import { useSearchParams } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ParamsSchema } from '../config/types';
import {
videoParamsConfig,
videoSizeOptions as videoSizeList
} from '../config/video-parameters';
import {
openaiCompatibleFieldsDefaultValus,
VIDEO_METAKEYS,
videoAdvancedDefaultValues
} from './config';
const videoInitialValues = {
size: '720x1280',
seconds: '4'
};
interface MessageProps {
modelList: Global.BaseOption<string>[];
model?: string;
loaded?: boolean;
isChat?: boolean;
ref?: any;
}
export const useInitVideoMeta = (
props: MessageProps,
options: { type: string }
) => {
const { modelList } = props;
const videoAdvancedParamsConfig: ParamsSchema[] = [];
const videoExtraConfig: ParamsSchema[] = [];
const form = useRef<any>(null);
const [searchParams] = useSearchParams();
const [modelMeta, setModelMeta] = useState<any>({});
const [isOpenaiCompatible, setIsOpenaiCompatible] = useState<boolean>(false);
const [videoSizeOptions, setVideoSizeOptions] = React.useState<
ImageSizeItem[]
>([]);
const [basicParamsConfig, setBasicParamsConfig] = React.useState<
ParamsSchema[]
>([...videoParamsConfig]);
const initialSeed = generateRandomNumber();
const [initialValues, setInitialValues] = useState<any>({
...videoInitialValues,
...videoAdvancedDefaultValues,
seed: initialSeed,
model: ''
});
const [paramsConfig, setParamsConfig] = useState<ParamsSchema[]>([
...videoParamsConfig,
...videoAdvancedParamsConfig
]);
const [parameters, setParams] = useState<any>({
...videoInitialValues,
...videoAdvancedDefaultValues,
seed: initialSeed,
model: ''
});
const defaultModel = useMemo(() => {
return searchParams.get('model') || modelList?.[0]?.value || '';
}, [modelList]);
const cacheFormData = React.useRef<Record<string, any>>({
...videoInitialValues,
...openaiCompatibleFieldsDefaultValus,
...videoAdvancedDefaultValues
});
const getNewSizeOptions = (metaData: any) => {
const { max_height, max_width } = metaData || {};
if (!max_height || !max_width) {
return videoSizeList;
}
const newSizeOptions = videoSizeList.filter((item) => {
return (
(item.width <= max_width && item.height <= max_height) ||
item.value === 'custom'
);
});
if (
!newSizeOptions.find(
(item) => item.width === max_width && item.height === max_height
)
) {
newSizeOptions.push({
width: max_width,
height: max_height,
label: `${max_width}x${max_height}`,
value: `${max_width}x${max_height}`
});
}
return newSizeOptions;
};
const generateParamsConfig = (
currentModel: any,
sizeOptions: ImageSizeItem[]
) => {
if (sizeOptions.length) {
const sizeConfig = videoParamsConfig.map((item) => {
return {
...item,
options: sizeOptions
};
});
return [...sizeConfig];
}
const { max_height, max_width } = currentModel.meta || {};
// generate custom size config
const customSizeConfig = _.cloneDeep(ImageCustomSizeConfig).map(
(item: any) => {
const max = item.name === 'height' ? max_height : max_width;
return {
...item,
attrs: {
...item.attrs,
max: max || item.attrs.max
}
};
}
);
return [...customSizeConfig];
};
const extractIMGMeta = (meta: any) => {
const sizeOptions = getNewSizeOptions(meta);
return {
form: _.merge(
{},
{ ...videoInitialValues, ...videoAdvancedDefaultValues },
{
..._.pick(meta, VIDEO_METAKEYS),
size: sizeOptions.length
? `${meta?.default_width || 720}x${meta?.default_height || 1280}`
: 'custom',
width: meta?.default_width || 720,
height: meta?.default_height || 1280
}
),
meta: meta,
sizeOptions: sizeOptions
};
};
const updateParamsConfig = (values: {
size: string;
isOpenaiCompatible: boolean;
}) => {
// update config
if (values.size === 'custom') {
return [
...basicParamsConfig,
...ImageCustomSizeConfig,
...(values.isOpenaiCompatible
? videoExtraConfig
: videoAdvancedParamsConfig)
];
}
return [
...basicParamsConfig,
...(values.isOpenaiCompatible
? videoExtraConfig
: videoAdvancedParamsConfig)
];
};
const updateCacheFormData = (values: Record<string, any>) => {
_.merge(cacheFormData.current, values);
};
const handleToggleParamsStyle = () => {
// update values
let values: any = {};
if (!isOpenaiCompatible) {
values = _.pick(
cacheFormData.current,
_.keys({
...videoInitialValues,
...openaiCompatibleFieldsDefaultValus
})
);
} else {
values = _.pick(
cacheFormData.current,
_.keys({
...videoInitialValues,
...videoAdvancedDefaultValues
})
);
}
// update config
const newParamsConfig = updateParamsConfig({
size: values.size,
isOpenaiCompatible: !isOpenaiCompatible
});
form.current?.form?.setFieldsValue({
...values,
model: parameters.model
});
setParamsConfig(newParamsConfig);
setParams({
...values,
model: parameters.model
});
setIsOpenaiCompatible(!isOpenaiCompatible);
updateCacheFormData({
...values,
model: parameters
});
};
const formFields = useMemo(() => {
const fields = paramsConfig?.map((item) => item.name);
return fields?.join(',');
}, [paramsConfig]);
const handleOnModelChange = useMemoizedFn((val: string) => {
if (!val) return;
const model = modelList.find((item) => item.value === val);
const { form: initialData, sizeOptions } = extractIMGMeta(model?.meta);
const newParamsConfig = generateParamsConfig(model, sizeOptions);
if (!isOpenaiCompatible) {
setParamsConfig([...newParamsConfig, ...videoAdvancedParamsConfig]);
} else {
setParamsConfig(newParamsConfig);
}
setBasicParamsConfig(newParamsConfig);
setVideoSizeOptions(sizeOptions);
setModelMeta(model?.meta || {});
setInitialValues({
...initialData,
seed: parameters.seed,
model: val
});
setParams({
...initialData,
seed: parameters.seed,
model: val
});
updateCacheFormData(initialData);
});
const handleOnValuesChange = useMemoizedFn(
(changeValues: Record<string, any>, allValues: Record<string, any>) => {
console.log('changeValues', changeValues);
// model change will reset all values
if (changeValues.model) {
handleOnModelChange(changeValues.model);
return;
}
if (changeValues.size && changeValues.size === 'custom') {
setParamsConfig([
...basicParamsConfig,
...ImageCustomSizeConfig,
...(!isOpenaiCompatible
? videoAdvancedParamsConfig
: videoExtraConfig)
]);
setParams({
...allValues,
width: modelMeta.default_width || 720,
height: modelMeta.default_height || 1280
});
form.current?.form?.setFieldsValue({
width: modelMeta.default_width || 720,
height: modelMeta.default_height || 1280
});
updateCacheFormData(changeValues);
} else if (changeValues.size && parameters.size === 'custom') {
setParamsConfig([
...basicParamsConfig,
...(!isOpenaiCompatible
? videoAdvancedParamsConfig
: videoExtraConfig)
]);
setParams(allValues);
updateCacheFormData(changeValues);
} else if (_.isBoolean(changeValues.random_seed)) {
const seed = changeValues.random_seed
? generateRandomNumber()
: parameters.seed;
setParams({
...allValues,
seed: seed
});
form.current?.form?.setFieldsValue({
seed: seed
});
updateCacheFormData(changeValues);
} else {
setParams(allValues);
updateCacheFormData(changeValues);
}
}
);
useEffect(() => {
if (defaultModel && modelList.length) {
handleOnModelChange(defaultModel);
}
}, [defaultModel, modelList.length]);
return {
extractIMGMeta,
generateParamsConfig,
setVideoSizeOptions,
setBasicParamsConfig,
updateCacheFormData,
handleOnValuesChange,
handleToggleParamsStyle,
setParams,
setInitialValues,
updateParamsConfig,
setParamsConfig,
form,
modelMeta,
formFields,
paramsConfig,
initialValues,
parameters,
isOpenaiCompatible,
cacheFormData,
basicParamsConfig,
videoSizeOptions,
openaiCompatibleFieldsDefaultValus,
videoAdvancedDefaultValues,
videoAdvancedParamsConfig,
videoInitialValues,
ImageCustomSizeConfig,
videoExtraConfig
};
};
@@ -0,0 +1,210 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { extractErrorMessage, promptList } from '@/pages/playground/config';
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { createVideo } from '../apis';
export default function useTextVideo(props: any) {
const intl = useIntl();
const { scroller, paramsRef } = props;
const [loading, setLoading] = useState(false);
const [tokenResult, setTokenResult] = useState<any>(null);
const [videoList, setVideoList] = useState<
{
dataUrl: string;
height: number | string;
width: string | number;
maxHeight: string | number;
maxWidth: string | number;
uid: number;
span?: number;
loading?: boolean;
progress?: number;
preview?: boolean;
}[]
>([]);
const [currentPrompt, setCurrentPrompt] = useState('');
const messageId = useRef<number>(0);
const requestToken = useRef<any>(null);
const { initialize } = useOverlayScroller();
const { initialize: innitializeParams } = useOverlayScroller();
const requestIdRef = useRef<number>(0);
useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, [initialize]);
useEffect(() => {
if (paramsRef.current) {
innitializeParams(paramsRef.current);
}
}, [innitializeParams]);
const updateRequestId = () => {
requestIdRef.current = requestIdRef.current + 1;
return requestIdRef.current;
};
const removeBase64Suffix = (str: string, suffix: string) => {
return str.endsWith(suffix) ? str.slice(0, -suffix.length) : str;
};
const setImageSize = (parameters: any) => {
let size: Record<string, string | number> = {
span: 12
};
if (parameters.n === 1) {
size.span = 24;
}
if (parameters.n === 2) {
size.span = 12;
}
if (parameters.n === 3) {
size.span = 12;
}
if (parameters.n === 4) {
size.span = 12;
}
return size;
};
const setMessageId = () => {
messageId.current = messageId.current + 1;
return messageId.current;
};
const generateNumber = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};
const stopDebounce = _.debounce(() => {
setVideoList([]);
setLoading(false);
}, 200);
const submitMessage = async (parameters: any) => {
try {
if (!parameters.model) return;
requestToken.current?.abort?.('cancel');
requestToken.current = new AbortController();
const currentRequestId = updateRequestId();
const size: any = setImageSize(parameters);
setLoading(true);
setMessageId();
setTokenResult(null);
const imgSize = _.split(parameters.size, 'x').map((item: string) =>
_.toNumber(item)
);
let newImageList = Array(parameters.n)
.fill({})
.map((item, index: number) => {
return {
dataUrl: 'data:image/png;base64,',
...size,
progress: 0,
height: imgSize[1],
width: imgSize[0],
loading: true,
progressType: 'dashboard',
preview: false,
uid: setMessageId()
};
});
setVideoList(newImageList);
const result = await createVideo({
data: parameters,
signal: requestToken.current.signal
});
console.log('result:', result);
if (result.error) {
setTokenResult({
error: true,
errorMessage: extractErrorMessage(result)
});
setVideoList([]);
return;
}
// If the request ID has changed, ignore this chunk
if (requestIdRef.current !== currentRequestId) {
return;
}
if (result?.error) {
setTokenResult({
error: true,
errorMessage: extractErrorMessage(result)
});
return;
}
result?.data?.forEach((item: any, index: number) => {
const imgItem = newImageList[index];
if (item.b64_json) {
imgItem.dataUrl = `data:image/png;base64,${item.b64_json}`;
}
newImageList[index] = {
dataUrl: imgItem.dataUrl,
height: imgSize[1],
width: imgSize[0],
maxHeight: `${imgSize[1]}px`,
maxWidth: `${imgSize[0]}px`,
uid: imgItem.uid,
span: imgItem.span,
loading: false,
preview: true,
progress: 100
};
});
console.log('newImageList:', newImageList);
setVideoList([...newImageList]);
} catch (error) {
console.log('error:', error);
updateRequestId();
stopDebounce();
requestToken.current?.abort?.('cancel');
} finally {
setLoading(false);
}
};
const handleClear = () => {
setVideoList([]);
setTokenResult(null);
setCurrentPrompt('');
};
const handleStopConversation = () => {
requestToken.current?.abort?.('stop');
};
useEffect(() => {
return () => {
requestToken.current?.abort?.('cancel');
};
}, []);
return {
loading,
tokenResult,
videoList,
promptList,
currentPrompt,
setVideoList,
setTokenResult,
setCurrentPrompt,
handleStopConversation,
generateNumber,
handleClear,
submitMessage
};
}
+116
View File
@@ -0,0 +1,116 @@
import HotKeys from '@/config/hotkeys';
import { ExtraContent } from '@/layouts/extraRender';
import { modelCategoriesMap } from '@/pages/llmodels/config';
import { useIntl } from '@umijs/max';
import useMemoizedFn from 'ahooks/lib/useMemoizedFn';
import { Divider } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { PageContainerInner } from '../_components/page-box';
import { queryModelsList } from './apis';
import GroundVideo from './components/ground-video';
import ViewCodeButtons from './components/view-code-buttons';
import useCollapseLayout from './hooks/use-collapse-layout';
import './style/play-ground.less';
const PlaygroundRerank: React.FC = () => {
const intl = useIntl();
const groundRerankerRef = useRef<any>(null);
const [rerankerModelList, setRerankerModelList] = useState<
Global.BaseOption<string>[]
>([]);
const [loaded, setLoaded] = useState(false);
useCollapseLayout({
handler: () => {
groundRerankerRef.current?.setCollapse?.();
},
triggeredRef: groundRerankerRef.current
});
const handleViewCode = useMemoizedFn(() => {
groundRerankerRef.current?.viewCode?.();
});
const handleToggleCollapse = useMemoizedFn(() => {
groundRerankerRef.current?.setCollapse?.();
});
useEffect(() => {
const getModelListByReranker = async () => {
try {
const params = {
categories: modelCategoriesMap.reranker,
with_meta: true
};
const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => {
return {
value: item.id,
label: item.id,
meta: item.meta
};
}) as Global.BaseOption<string>[];
return list;
} catch (error) {
console.error(error);
return [];
}
};
const fetchData = async () => {
try {
const [rerankerModelList] = await Promise.all([
getModelListByReranker()
]);
setRerankerModelList(rerankerModelList);
} catch (error) {
setLoaded(true);
}
};
fetchData();
}, []);
useHotkeys(
HotKeys.RIGHT.join(','),
() => {
groundRerankerRef.current?.setCollapse?.();
},
{
preventDefault: true
}
);
return (
<PageContainerInner
className={classNames('playground-container chat')}
extra={[
<ViewCodeButtons
activeKey=""
handleViewCode={handleViewCode}
handleToggleCollapse={handleToggleCollapse}
key="view-code-buttons"
></ViewCodeButtons>,
<Divider
key="divider"
orientation="vertical"
style={{ height: 24, marginInline: 16 }}
/>,
<ExtraContent key="extra-content" />
]}
>
<div className="play-ground">
<div className="chat">
<GroundVideo
ref={groundRerankerRef}
modelList={rerankerModelList}
loaded={loaded}
></GroundVideo>
</div>
</div>
</PageContainerInner>
);
};
export default PlaygroundRerank;
+86
View File
@@ -0,0 +1,86 @@
import _ from 'lodash';
import { MODEL_PROXY } from '../apis';
import { fomatNodeJsParams, formatCurlArgs } from './utils';
export const generateCurlCode = ({
api: url,
parameters,
modelProxy,
isFormdata = false,
edit = false
}: Record<string, any>) => {
const host = window.location.origin;
const api = modelProxy ? `${MODEL_PROXY}/\${YOUR_API_PATH}` : url;
// ========================= Curl =========================
let curlCode = `
curl ${host}${api} \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer $\{YOUR_GPUSTACK_API_KEY}" \\${modelProxy ? `\n-H "X-GPUStack-Model: ${parameters.model}" \\` : ''}
${formatCurlArgs(parameters, isFormdata)}`.trim();
if (edit) {
curlCode = `
curl ${host}${api} \\
-H "Content-Type: multipart/form-data" \\
-H "Authorization: Bearer $\{YOUR_GPUSTACK_API_KEY}" \\${modelProxy ? `\n-H "X-GPUStack-Model: ${parameters.model}" \\` : ''}
-F image="@image.png" \\
-F mask="@mask.png" \\
${formatCurlArgs(_.omit(parameters, ['mask', 'image']), isFormdata)}`
.trim()
.replace(/\\$/, '');
}
return curlCode;
};
export const generateCode = ({
api: url,
parameters,
isFormdata = false,
edit = false
}: Record<string, any>) => {
const host = window.location.origin;
const api = url;
// ========================= Curl =========================
let curlCode = generateCurlCode({
api: url,
parameters,
isFormdata,
edit
});
// ========================= Python =========================
const pythonCode = `
import requests\n
url="${host}${api}"
headers = {
"Content-type": "application/json",
"Authorization": "Bearer $\{YOUR_GPUSTACK_API_KEY}"
}
data = ${JSON.stringify(parameters, null, 2).replace(/null/g, 'None')}\n
response = requests.post(url, headers=headers, json=data)
print(response.json()['data']['object'])`.trim();
// ========================= Node.js =========================
const nodeJsCode = `
const axios = require('axios');
const url = "${host}${api}";
const headers = {
"Content-type": "application/json",
"Authorization": "Bearer $\{YOUR_GPUSTACK_API_KEY}"
};
const data = ${fomatNodeJsParams(parameters)};
axios.post(url, data, { headers }).then((response) => {
console.log(response.data.object);
});`.trim();
return {
curlCode,
pythonCode,
nodeJsCode
};
};