[New Feature] Support SAM 2.1 (#59)

* support sam 2.1

* refine config path and ckpt path

* update README
This commit is contained in:
Ren Tianhe
2024-10-10 14:55:50 +08:00
committed by GitHub
parent e899ad99e8
commit 82e503604f
340 changed files with 39100 additions and 608 deletions

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Tooltip from '@/common/components/Tooltip';
import {ArrowPathIcon, CheckIcon, XMarkIcon} from '@heroicons/react/24/solid';
import {ChangeEvent, KeyboardEvent, useEffect, useMemo, useState} from 'react';
import {Button, Form, Input, Join} from 'react-daisyui';
type Props<T extends string | number> = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'size' | 'color' | 'onChange'
> & {
label: string;
defaultValue: T;
initialValue: T;
onChange: (value: string) => void;
};
function getStep(value: number) {
const stringValue = String(value);
const decimals = stringValue.split('.')[1];
if (decimals != null) {
// Not using 0.1 ** decimals.length because this will result in rounding
// errors, e.g., 0.1 ** 2 => 0.010000000000000002.
return 1 / 10 ** decimals.length;
}
return 1;
}
export default function ApprovableInput<T extends string | number>({
label,
defaultValue,
initialValue,
onChange,
...otherProps
}: Props<T>) {
const [value, setValue] = useState<string>(`${initialValue}`);
useEffect(() => {
setValue(`${initialValue}`);
}, [initialValue]);
const step = useMemo(() => {
return typeof defaultValue === 'number' && isFinite(defaultValue)
? getStep(defaultValue)
: undefined;
}, [defaultValue]);
return (
<div>
<Form.Label className="flex-col items-start gap-2" title={label}>
<Join className="w-full">
<Input
{...otherProps}
className="w-full join-item"
value={value}
step={step}
placeholder={`${defaultValue}`}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
onChange(value);
}
}}
/>
<Tooltip message="Reset to default">
<Button
className="join-item"
onClick={event => {
event.preventDefault();
setValue(`${defaultValue}`);
}}>
<ArrowPathIcon className="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip message="Revert change">
<Button
className="join-item"
color="neutral"
disabled={initialValue == value}
onClick={event => {
event.preventDefault();
setValue(`${initialValue}`);
}}>
<XMarkIcon className="h-4 w-4" />
</Button>
</Tooltip>
<Tooltip message="Apply change">
<Button
className="join-item"
color="primary"
disabled={initialValue == value}
onClick={event => {
event.preventDefault();
onChange(value);
}}>
<CheckIcon className="h-4 w-4" />
</Button>
</Tooltip>
</Join>
</Form.Label>
</div>
);
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
import ApprovableInput from '@/settings/ApprovableInput';
import useSettingsContext from '@/settings/useSettingsContext';
export default function SAMVSettings() {
const {settings, dispatch} = useSettingsContext();
return (
<div>
<ApprovableInput
label="Video API Endpoint"
defaultValue={VIDEO_API_ENDPOINT}
initialValue={settings.videoAPIEndpoint}
onChange={url => dispatch({type: 'change-video-api-endpoint', url})}
/>
<ApprovableInput
label="Inference API Endpoint"
defaultValue={INFERENCE_API_ENDPOINT}
initialValue={settings.inferenceAPIEndpoint}
onChange={url => dispatch({type: 'change-inference-api-endpoint', url})}
/>
</div>
);
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import emptyFunction from '@/common/utils/emptyFunction';
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
import SettingsModal from '@/settings/SettingsModal';
import {
Action,
DEFAULT_SETTINGS,
Settings,
settingsReducer,
} from '@/settings/SettingsReducer';
import {
PropsWithChildren,
createContext,
useCallback,
useMemo,
useRef,
} from 'react';
import {useImmerReducer} from 'use-immer';
type ContextProps = {
settings: Settings;
dispatch: React.Dispatch<Action>;
openModal: () => void;
closeModal: () => void;
hasChanged: boolean;
};
export const SettingsContext = createContext<ContextProps>({
settings: DEFAULT_SETTINGS,
dispatch: emptyFunction,
openModal: emptyFunction,
closeModal: emptyFunction,
hasChanged: false,
});
type Props = PropsWithChildren;
export default function SettingsContextProvider({children}: Props) {
const [state, dispatch] = useImmerReducer(
settingsReducer,
DEFAULT_SETTINGS,
settings => {
// Load the settings from local storage. Eventually use the reducer init
// to handle initial loading.
return settingsReducer(settings, {type: 'load-state'});
},
);
const modalRef = useRef<HTMLDialogElement>(null);
const openModal = useCallback(() => {
modalRef.current?.showModal();
}, [modalRef]);
const handleCloseModal = useCallback(() => {
modalRef.current?.close();
}, [modalRef]);
const hasChanged = useMemo(() => {
return (
VIDEO_API_ENDPOINT !== state.videoAPIEndpoint ||
INFERENCE_API_ENDPOINT !== state.inferenceAPIEndpoint
);
}, [state.videoAPIEndpoint, state.inferenceAPIEndpoint]);
const value = useMemo(
() => ({
settings: state,
dispatch,
openModal,
closeModal: handleCloseModal,
hasChanged,
}),
[state, dispatch, openModal, handleCloseModal, hasChanged],
);
return (
<SettingsContext.Provider value={value}>
{children}
<SettingsModal ref={modalRef} />
</SettingsContext.Provider>
);
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {DEMO_FRIENDLY_NAME} from '@/demo/DemoConfig';
import SAM2Settings from '@/settings/SAM2Settings';
import {XMarkIcon} from '@heroicons/react/24/solid';
import {forwardRef, useState} from 'react';
import {Button, Modal} from 'react-daisyui';
import useSettingsContext from './useSettingsContext';
type Props = unknown;
type Config = {
key: 'sam2';
title: string;
component: React.ElementType;
};
const SettingsConfig: Config[] = [
{
key: 'sam2',
title: DEMO_FRIENDLY_NAME,
component: SAM2Settings,
},
];
export default forwardRef<HTMLDialogElement, Props>(
function SettingsModal(_props, ref) {
const {closeModal} = useSettingsContext();
const [activeConfig, setActiveConfig] = useState<Config>(SettingsConfig[0]);
const SettingsComponent = activeConfig.component;
return (
<Modal
data-testid="settings-modal"
ref={ref}
className="lg:absolute lg:top-10 lg:w-11/12 lg:max-w-4xl flex flex-col"
responsive={true}>
<Button
size="sm"
color="ghost"
shape="circle"
className="absolute right-2 top-2"
startIcon={<XMarkIcon className="w-6 h-6" />}
onClick={closeModal}
/>
<Modal.Header className="font-bold">Settings</Modal.Header>
<Modal.Body className="flex flex-col grow overflow-hidden">
<div className="flex flex-col md:lg:flex-row gap-4 md:lg:gap-12 overflow-hidden">
<div className="flex flex-row shrink-0 md:lg:flex-col gap-4 md:lg:py-2 overflow-x-auto">
{SettingsConfig.map(config => (
<div
key={config.key}
data-testid={`show-settings-${config.key}`}
className={`cursor-pointer whitespace-nowrap ${
activeConfig.key === config.key && 'text-primary'
} ${
activeConfig.key === config.key &&
'sm:underline md:lg:no-underline sm:underline-offset-4'
}`}
onClick={() => setActiveConfig(config)}>
{config.title}
</div>
))}
</div>
<div
data-testid={`settings-${activeConfig.key}`}
className="overflow-hidden overflow-y-auto grow md:lg:pt-2">
<div className="flex flex-col grow-0 flex-1">
<h1 className="hidden md:lg:block">{activeConfig.title}</h1>
<SettingsComponent />
</div>
</div>
</div>
</Modal.Body>
<Modal.Actions className="shrink-0">
<Button onClick={closeModal}>Close</Button>
</Modal.Actions>
</Modal>
);
},
);

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {INFERENCE_API_ENDPOINT, VIDEO_API_ENDPOINT} from '@/demo/DemoConfig';
export type Settings = {
videoAPIEndpoint: string;
inferenceAPIEndpoint: string;
};
// Key used to store the settings in the browser's local storage.
export const SAM2_SETTINGS_KEY = 'SAM2_SETTINGS_KEY';
export type Action =
| {type: 'load-state'}
| {type: 'change-video-api-endpoint'; url: string}
| {type: 'change-inference-api-endpoint'; url: string};
export const DEFAULT_SETTINGS: Settings = {
videoAPIEndpoint: VIDEO_API_ENDPOINT,
inferenceAPIEndpoint: INFERENCE_API_ENDPOINT,
};
export function settingsReducer(state: Settings, action: Action): Settings {
function storeSettings(newState: Settings): void {
localStorage.setItem(SAM2_SETTINGS_KEY, JSON.stringify(newState));
}
switch (action.type) {
case 'load-state': {
try {
const serializedSettings = localStorage.getItem(SAM2_SETTINGS_KEY);
if (serializedSettings != null) {
return JSON.parse(serializedSettings) as Settings;
} else {
// Store default settings in local storage. This will populate the
// settings in the local storage on first app load or when user
// cleared the browser cache.
storeSettings(state);
}
} catch {
// Could not parse settings. Using default settings instead.
}
return state;
}
case 'change-video-api-endpoint':
state.videoAPIEndpoint = action.url;
break;
case 'change-inference-api-endpoint':
state.inferenceAPIEndpoint = action.url;
break;
}
// Store the settings state on every change
storeSettings(state);
return state;
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {useContext} from 'react';
import {SettingsContext} from '@/settings/SettingsContextProvider';
export default function useSettingsContext() {
return useContext(SettingsContext);
}