Create monorepo from known-good production state

This commit is contained in:
makearmy 2026-07-09 21:07:34 -04:00
commit c034824338
651 changed files with 120469 additions and 0 deletions

39
bgbye/src/App.css Normal file
View file

@ -0,0 +1,39 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

93
bgbye/src/App.js Normal file
View file

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { Box, ThemeProvider, createTheme, Paper, FormGroup, FormControlLabel, Checkbox, Divider, Typography } from '@mui/material';
import { Toaster, toast } from 'react-hot-toast';
import ImageUpload from './components/ImageUpload';
import ResponsiveAppBar from './components/ResponsiveAppBar';
import MethodSelector from './components/MethodSelector';
import ModelsInfo from './components/ModelsInfo';
// Utility function to show error toast
const showErrorToast = (message) => {
toast.error(message, {
duration: 4000,
position: 'bottom-center',
style: {
background: '#FF4136',
color: '#FFFFFF',
},
});
};
const APP_ID = 'bgbye';
function App() {
const [processedPanels, setProcessedPanels] = useState(0);
const [darkMode, setDarkMode] = useState(() => {
const storedTheme = localStorage.getItem(APP_ID + '_theme');
return storedTheme !== null ? storedTheme === 'true' : false;
});
const [selectedModels, setSelectedModels] = useState(
Object.keys(ModelsInfo).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {})
);
const handleProcessed = () => {
setProcessedPanels((prevCount) => prevCount + 1);
};
const toggleTheme = () => {
setDarkMode((prevMode) => !prevMode);
localStorage.setItem(APP_ID + '_theme', !darkMode);
};
const handleModelChange = (event) => {
setSelectedModels({
...selectedModels,
[event.target.name]: event.target.checked
});
};
const theme = createTheme({
palette: {
mode: darkMode ? 'dark' : 'light',
background: {
default: darkMode
? 'linear-gradient(180deg, #10364a 30%, #0d171c 90%)'
: 'linear-gradient(180deg, #cbdbf2 30%, #b0ccff 90%)',
},
},
});
return (
<ThemeProvider theme={theme}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
minHeight: '100vh',
background: theme.palette.background.default,
}}
>
<ResponsiveAppBar toggleTheme={toggleTheme} darkMode={darkMode}/>
{Array.from({ length: processedPanels + 1 }).map((_, index) => (
<Box key={index} sx={{ mt: 4 }}>
<ImageUpload
onProcessed={handleProcessed}
theme={theme}
fileID={index}
selectedModels={selectedModels}
showErrorToast={showErrorToast}
/>
</Box>
))}
<MethodSelector selectedModels={selectedModels} handleModelChange={handleModelChange} />
</Box>
<Toaster />
</ThemeProvider>
);
}
export default App;

8
bgbye/src/App.test.js Normal file
View file

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

7
bgbye/src/TODO.txt Normal file
View file

@ -0,0 +1,7 @@
Improve UI:
add dropdown for download types
- webp/png
- mov/gif/webm
add download video progressbar

View file

@ -0,0 +1,48 @@
import React from 'react';
import Button from '@mui/material/Button';
import Popover from '@mui/material/Popover';
import ColorPicker from 'react-best-gradient-color-picker';
import ColorizeIcon from '@mui/icons-material/Colorize';
function GradientPickerPopout({ buttonLabel, color, onChange }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleChange = (newColor) => {
if (onChange) {
onChange(newColor);
}
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
return (
<div>
<Button aria-describedby={id} variant="contained" onClick={handleClick} endIcon={<ColorizeIcon/>} fullWidth>
{buttonLabel}
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<ColorPicker value={color} onChange={handleChange} />
</Popover>
</div>
);
}
export default GradientPickerPopout;

View file

@ -0,0 +1,630 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import {
Box,
Button,
Typography,
useTheme,
CircularProgress,
ToggleButtonGroup,
ToggleButton,
Select,
MenuItem,
FormControl,
InputLabel,
LinearProgress,
Paper,
Checkbox,
FormControlLabel,
useMediaQuery,
} from '@mui/material';
import DownloadIcon from '@mui/icons-material/Download';
import axios from 'axios';
import { ImgComparisonSlider } from '@img-comparison-slider/react';
import pLimit from 'p-limit';
import GradientPickerPopout from './GradientPickerPopout';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import Magnifier from 'react18-image-magnifier'
import ModelsInfo from './ModelsInfo';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import GradientIcon from '@mui/icons-material/Gradient';
const ImageUpload = ({ onProcessed, fileID, selectedModels, showErrorToast }) => {
const [selectedFile, setSelectedFile] = useState(null);
const [originalFilename, setOriginalFilename] = useState('');
const [processedFiles, setProcessedFiles] = useState({});
const [activeMethod, setActiveMethod] = useState(null);
const [processing, setProcessing] = useState({});
const [localSelectedModels, setLocalSelectedModels] = useState(null);
const [fileType, setFileType] = useState(null);
const [videoMethod, setVideoMethod] = useState('');
const [videoId, setVideoId] = useState(null);
const [videoProgress, setVideoProgress] = useState(0);
const [statusMessage, setStatusMessage] = useState('');
const [dragOver, setDragOver] = useState(false);
const [doZoom, setDoZoom] = useState(false);
const [transparent, setTransparent] = useState(true);
const [colorBG, setColorBG] = useState('radial-gradient(circle, #fcdfa4 0%, #ffd83b 100%)'); //useState('radial-gradient(circle, #87CEFA 0%, #1E90FF 100%)');
const [imageWidth, setImageWidth] = useState('500px'); // Default width
useEffect(() => {
if (selectedFile) {
const image = new Image();
image.onload = () => {
setImageWidth(`${image.width}px`);
};
image.src = selectedFile;
}
}, [selectedFile]);
const theme = useTheme();
const isPortrait = useMediaQuery('(orientation: portrait)');
const fileInputID = "fileInput" + fileID.toString();
const getModelAPIURL = (method)=>{
console.log(method, ModelsInfo[method].apiUrlVar, process.env[ModelsInfo[method].apiUrlVar]);
return process.env[ModelsInfo[method].apiUrlVar];
}
useEffect(() => {
if (!localSelectedModels) {
setLocalSelectedModels(selectedModels);
}
}, [selectedModels, localSelectedModels]);
const dropZoneRef = useRef(null);
const processFile = useCallback(async (file, method) => {
const formData = new FormData();
formData.append('file', file);
formData.append('method', method);
const isVideo = file.type.startsWith('video');
const endpoint = isVideo ? 'remove_background_video' : 'remove_background';
try {
const response = await axios.post(`${getModelAPIURL(method)}/${endpoint}/`, formData, {
responseType: 'blob',
withCredentials: false,
});
if (response) {
const fileUrl = URL.createObjectURL(response.data);
return fileUrl;
}
} catch (error) {
console.error(`Error processing ${isVideo ? 'video' : 'image'} with ${method}:`, error);
showErrorToast(`Error processing ${isVideo ? 'video' : 'image'} with ${method}`);
}
return null;
}, [showErrorToast]);
const handleFileUpload = useCallback(async (event) => {
const file = event.target.files[0];
if (!file) return;
const isVideo = file.type.startsWith('video');
setFileType(isVideo ? 'video' : 'image');
setSelectedFile(URL.createObjectURL(file));
setOriginalFilename(file.name);
setProcessedFiles({});
setActiveMethod(null);
const currentSelectedModels = {...selectedModels};
setLocalSelectedModels(currentSelectedModels);
if (!isVideo) {
const initialProcessing = Object.fromEntries(
Object.entries(currentSelectedModels)
.filter(([_, isSelected]) => isSelected)
.map(([method, _]) => [method, true])
);
setProcessing(initialProcessing);
// Create a limit function that allows only 6 concurrent operations
const limit = pLimit(6);
// Create an array of promises
const promises = Object.entries(currentSelectedModels)
.filter(([_, isSelected]) => isSelected)
.map(([method, _]) =>
limit(() => processFile(file, method).then(result => {
setProcessedFiles(prev => ({...prev, [method]: result}));
setProcessing(prev => ({...prev, [method]: false}));
if (!activeMethod) {
setActiveMethod(method);
}
}))
);
// Wait for all promises to resolve
await Promise.all(promises);
}
onProcessed();
}, [processFile, onProcessed, selectedModels, activeMethod]);
const handleDragOver = (event) => {
event.preventDefault();
setDragOver(true);
};
const handleDragLeave = (event) => {
event.preventDefault();
setDragOver(false);
};
const handleDrop = useCallback((event) => {
event.preventDefault();
setDragOver(false);
const file = event.dataTransfer.files[0];
if (file) {
const fakeEvent = { target: { files: [file] } };
handleFileUpload(fakeEvent);
}
}, [handleFileUpload]);
const handlePaste = useCallback((event) => {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const blob = items[i].getAsFile();
const file = new File([blob], "pasted-image.png", { type: blob.type });
const fakeEvent = { target: { files: [file] } };
handleFileUpload(fakeEvent);
break;
}
}
}, [handleFileUpload]);
useEffect(() => {
const dropZone = dropZoneRef.current;
if (dropZone) {
dropZone.addEventListener('paste', handlePaste);
return () => {
dropZone.removeEventListener('paste', handlePaste);
};
}
}, [handlePaste]);
const handleMethodChange = (event, newMethod) => {
if (newMethod !== null) {
setActiveMethod(newMethod);
}
};
const handleVideoMethodChange = (event) => {
setVideoMethod(event.target.value);
};
const pollVideoStatus = useCallback(async (id, url) => {
try {
const response = await axios.get(`${url}/status/${id}`, {
responseType: 'blob',
withCredentials:false
});
// Check if the response is JSON (status update) or blob (completed video)
const contentType = response.headers['content-type'];
if (contentType && contentType.indexOf('application/json') !== -1) {
// It's a JSON response (status update)
const data = await response.data.text().then(JSON.parse);
if (data.status === 'processing') {
setVideoProgress(data.progress);
setStatusMessage(data.message);
setTimeout(() => pollVideoStatus(id, url), 4000); // Poll every second
} else if (data.status === 'error') {
showErrorToast('Error processing video: ' + data.message);
setProcessing({ [videoMethod]: false });
setStatusMessage('Error: ' + data.message);
}
} else {
// It's a blob response (completed video)
setVideoProgress(100);
setProcessing({ [videoMethod]: false });
setActiveMethod(videoMethod);
setStatusMessage('Processing complete');
const url = URL.createObjectURL(response.data);
setProcessedFiles({ [videoMethod]: url });
}
} catch (error) {
console.error('Error polling video status:', error);
setProcessing({ [videoMethod]: false });
setStatusMessage('Error: Failed to get status update');
showErrorToast('Error: Failed to get status update');
}
}, [videoMethod, showErrorToast]);
const getVideoDuration = (file) => {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = function() {
window.URL.revokeObjectURL(video.src);
resolve(video.duration);
}
video.onerror = function() {
reject("Invalid video. Please select another video file.");
}
video.src = URL.createObjectURL(file);
});
}
const handleProcessVideo = async () => {
if (!selectedFile || !videoMethod) return;
setProcessing({ [videoMethod]: true });
setVideoProgress(0);
try {
const file = await fetch(selectedFile).then(r => r.blob());
// Estimate frame count
const duration = await getVideoDuration(file);
const estimatedFrameCount = Math.ceil(duration * 24); // Assuming 24 fps
//DISABLED VIDEO LENGTH LIMIT
//if (estimatedFrameCount > 250) {
// if (duration > 10){
// showErrorToast(`Video too long (${estimatedFrameCount} estimated frames). Maximum allowed: 10 seconds.`);
// setProcessing({ [videoMethod]: false });
// return;
// }
const formData = new FormData();
formData.append('file', file);
formData.append('method', videoMethod);
const response = await axios.post(`${getModelAPIURL(videoMethod)}/remove_background_video/`, formData, {
withCredentials: false,
});
setVideoId(response.data.video_id);
pollVideoStatus(response.data.video_id, getModelAPIURL(videoMethod));
} catch (error) {
console.error('Error processing video:', error);
setProcessing({ [videoMethod]: false });
showErrorToast('Error processing video: ', error);
}
};
const handleDownload = () => {
if (fileType === 'video') {
// Directly download the video file as a .webm
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.webm`;
const link = document.createElement('a');
link.href = processedFiles[activeMethod];
link.download = newFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
// Handle image downloading with canvas manipulations
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const image = new Image();
image.src = processedFiles[activeMethod];
image.onload = () => {
canvas.width = image.width;
canvas.height = image.height;
if (fileType === 'image' && transparent === false) {
if (colorBG.includes("gradient")) {
const tempDiv = document.createElement("div");
tempDiv.style.display = 'none'; // Hide the div while it's appended
tempDiv.style.background = colorBG;
document.body.appendChild(tempDiv);
const computedStyle = window.getComputedStyle(tempDiv);
const bgImage = computedStyle.backgroundImage;
document.body.removeChild(tempDiv);
if (bgImage.startsWith('linear-gradient')) {
parseLinearGradient(ctx, bgImage, canvas.width, canvas.height);
} else if (bgImage.startsWith('radial-gradient')) {
parseRadialGradient(ctx, bgImage, canvas.width, canvas.height);
}
} else {
ctx.fillStyle = colorBG;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
ctx.drawImage(image, 0, 0);
const fileExtension = 'png';
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.${fileExtension}`;
const link = document.createElement('a');
link.href = canvas.toDataURL(`image/${fileExtension}`);
link.download = newFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
}
};
function parseLinearGradient(ctx, bgImage, width, height) {
const colors = bgImage.match(/rgba?\([^)]+\)/g);
const linearGradient = ctx.createLinearGradient(0, 0, width, 0);
colors.forEach((color, index) => {
const position = index / (colors.length - 1);
linearGradient.addColorStop(position, color);
});
ctx.fillStyle = linearGradient;
ctx.fillRect(0, 0, width, height);
}
function parseRadialGradient(ctx, bgImage, width, height) {
const colors = bgImage.match(/rgba?\([^)]+\)/g);
const radialGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 2);
colors.forEach((color, index) => {
const position = index / (colors.length - 1);
radialGradient.addColorStop(position, color);
});
ctx.fillStyle = radialGradient;
ctx.fillRect(0, 0, width, height);
}
return (
<Box
ref={dropZoneRef}
tabIndex="0" // Make the box focusable
onKeyDown={(e) => {
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
// This allows the paste event to fire when the box is focused
// and the user presses Ctrl+V or Cmd+V
}
}}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
border: (!selectedFile) ? '2px dashed' : 'none',
borderColor: dragOver ? theme.palette.primary.main : theme.palette.text.disabled,
borderRadius: 1,
p: isPortrait ? 0 : 2,
mt: 2,
textAlign: 'center',
cursor: !selectedFile && !processing ? 'pointer' : 'default',
position: 'relative',
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'transparent',
transition: 'all 0.3s ease',
}}
onClick={() => !selectedFile && !Object.values(processing).some(Boolean) && document.getElementById(fileInputID).click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{!selectedFile && (
<input
type="file"
id={fileInputID}
accept="image/*,video/*"
style={{ display: 'none' }}
onChange={handleFileUpload}
/>
)}
{selectedFile ? (
<Box sx={{
display: 'flex',
flexDirection: isPortrait ? 'column' : 'row',
alignItems: 'flex-start',
width: '100%',
maxWidth: '1024px',
position: 'relative',
}}>
<Box sx={{
flex: 1,
maxWidth: '1280px',
mr: isPortrait ? 0 : 2,
mb: isPortrait ? 2 : 0,
width: '100%',
}}>
{fileType === 'image' ? (
processedFiles[activeMethod] ? (
<div
className={transparent ? "checkerboard" : ""}
style={!transparent ? { background: colorBG } : {}}
>
<ToggleButton
value="zoom"
selected={doZoom}
onChange={() => setDoZoom(!doZoom)}
aria-label="zoom in"
size='small'
color='primary'
sx={{
position: 'absolute',
top: '3em',
left: '3em',
zIndex: 9999
}}
>
<ZoomInIcon color='primary'/>
</ToggleButton>
{doZoom && <Magnifier src={processedFiles[activeMethod]} width={'100%'}/>}
{!doZoom && <ImgComparisonSlider class="slider-example-focus">
<img slot="first" src={selectedFile} alt="Original" style={{ width: '100%' }} />
<img slot="second" src={processedFiles[activeMethod]} alt="Processed" style={{ width: '100%' }} />
{false && <svg slot="handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
<path stroke="#549ef7" d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2 M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2" strokeWidth="1" fill="#549ef7" vectorEffect="non-scaling-stroke"></path>
</svg>}
</ImgComparisonSlider>}
</div>
) : (
<>
<div style={{ position: 'relative', display: 'inline-block' }}>
<CircularProgress style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
<img src={selectedFile} alt="Uploaded" style={{ width: '100%', display: "block", boxShadow: '0px 0px 10px 5px #6464647a' }} />
</div>
</>
)
) : (
<video src={processedFiles[activeMethod] || selectedFile} controls style={{ width: '100%' }}>
Your browser does not support the video tag.
</video>
)}
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
position: isPortrait ? 'absolute' : 'static',
top: isPortrait ? '1em' : 'auto',
right: isPortrait ? '1em' : 'auto',
zIndex: isPortrait ? 1000 : 'auto',
}}>
{(!isPortrait || fileType !== 'video') && <Paper sx={{
backgroundColor: isPortrait ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0)',
padding: isPortrait ? 1 : 0,
}} elevation={2}>
<Typography variant="body2" color="text.secondary" align="center">
Methods
</Typography>
{selectedFile && localSelectedModels && fileType === 'image' && (
<ToggleButtonGroup
orientation="vertical"
fullWidth
value={activeMethod}
exclusive
color="warning"
onChange={handleMethodChange}
aria-label="background removal method"
sx={{
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
{Object.entries(localSelectedModels)
.filter(([_, isSelected]) => isSelected)
.map(([method, _]) => (
<ToggleButton
size="small"
key={method}
value={method}
aria-label={`${method} method`}
disabled={processing[method]}
sx={{
justifyContent: 'space-between',
}}
>
<Box display="flex" alignItems="center" justifyContent="flex-start" width="100%">
{isPortrait ? ModelsInfo[method].shortName : ModelsInfo[method].displayName}
{processing[method] && <CircularProgress size={16} sx={{ ml: 1 }} />}
</Box>
</ToggleButton>
))
}
</ToggleButtonGroup>
)}
</Paper>}
{selectedFile && fileType === 'video' && (
<Box sx={{ mt: 2 }}>
<FormControl fullWidth>
<InputLabel id="video-method-label" >Method</InputLabel>
<Select
disabled={processing[videoMethod] || Object.keys(processedFiles).length > 0}
labelId="video-method-label"
value={videoMethod}
label="Method"
sx={{backgroundColor:isPortrait? theme.palette.info.contrastText :''}}
onChange={handleVideoMethodChange}
>
{Object.entries(localSelectedModels)
.filter(([_, isSelected]) => isSelected)
.map(([method, _]) => (
<MenuItem key={method} value={method}>{ModelsInfo[method].displayName}</MenuItem>
))
}
</Select>
</FormControl>
{Object.keys(processedFiles).length === 0 && <Button
variant="contained"
color="primary"
onClick={handleProcessVideo}
disabled={!videoMethod || Object.values(processing).some(Boolean)}
sx={{ mt: 2 }}
>
Process Video
{processing[videoMethod] && <CircularProgress size={16} sx={{ ml: 1 }} />}
</Button>}
{processing[videoMethod] && (
<Box sx={{ width: '100%', mt: 2 }}>
<LinearProgress
variant={videoProgress === 100 ? "indeterminate" : "determinate"}
value={videoProgress}
/>
<Typography variant="body2" color="text.secondary" align="center">
{statusMessage} {videoProgress < 100 && `(${Math.round(videoProgress)}%)`}
</Typography>
</Box>
)}
</Box>
)}
{processedFiles[activeMethod] && (
<>
{!isPortrait && fileType=='image' && <FormControlLabel
control={<Checkbox checked={transparent} onChange={(e)=>setTransparent(e.target.checked)} />}
label="Transparent"
sx={{color:theme.palette.text.primary}}
/>}
{isPortrait && fileType=='image' && <ToggleButton sx={{backgroundColor:theme.palette.divider, p:0}} value="transparent" selected={!transparent} onChange={()=>{setTransparent(!transparent)}}><GradientIcon fontSize='large' color='primary'/></ToggleButton>}
{!transparent && fileType=='image' && <GradientPickerPopout
buttonLabel={!isPortrait ? "Background" : ""}
color={colorBG}
onChange={newColor => setColorBG(newColor)}
/>}
<Button
variant="contained"
color="primary"
onClick={handleDownload}
endIcon={<DownloadIcon />}
sx={{ mt: 2 }}
>
{!isPortrait && "Download"}
</Button>
</>
)}
</Box>
</Box>
) : (
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
{dragOver ? "Click, drag and drop, or paste (ctrl-v) to upload an image or video" : "Click, drag and drop, or paste (ctrl-v) to upload an image or video"}
</Typography>
)}
</Box>
);
};
export default ImageUpload;

View file

@ -0,0 +1,80 @@
import React, { useState } from 'react';
import { Button, Popover, FormGroup, FormControlLabel, Checkbox, Typography, Divider, IconButton } from '@mui/material';
import ChecklistIcon from '@mui/icons-material/Checklist';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import ModelsInfo from './ModelsInfo';
const MethodSelector = ({ selectedModels, handleModelChange }) => {
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'method-selector-popover' : undefined;
const handleInfoClick = (apiUrl) => {
window.open(apiUrl, '_blank');
};
return (
<>
<Button variant="contained" onClick={handleClick} endIcon={<ChecklistIcon/>} size='small' sx={{mt:1}}>
Select Methods
</Button>
<Popover
elevation={2}
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<FormGroup sx={{ p: 1 }}>
{Object.keys(selectedModels).map((model, index) => (
<React.Fragment key={model}>
<FormControlLabel
control={
<Checkbox
checked={selectedModels[model]}
onChange={handleModelChange}
name={model}
/>
}
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>{ModelsInfo[model].displayName}</span>
<IconButton
size="small"
onClick={() => handleInfoClick(ModelsInfo[model].sourceUrl)}
style={{ marginLeft: '8px' }}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
</div>
}
/>
{index < Object.keys(selectedModels).length - 1 && (
<Divider orientation="horizontal" flexItem />
)}
</React.Fragment>
))}
</FormGroup>
</Popover>
</>
);
};
export default MethodSelector;

View file

@ -0,0 +1,65 @@
// modelsInfo.js
const ModelsInfo = {
bria: {
displayName: 'Bria RMBG1.4',
shortName: "Bria",
sourceUrl: 'https://huggingface.co/briaai/RMBG-1.4',
apiUrlVar: 'REACT_APP_BRIA_URL'
},
inspyrenet: {
displayName: 'InSPyReNet',
shortName: "InSPyRe",
sourceUrl: 'https://github.com/plemeri/transparent-background/tree/main',
apiUrlVar: 'REACT_APP_INSPYRENET_URL'
},
u2net: {
displayName: 'U2Net',
shortName: "U2Net",
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
apiUrlVar: 'REACT_APP_U2NET_URL'
},
tracer: {
displayName: 'Tracer-B7',
shortName: "Tracer",
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
apiUrlVar: 'REACT_APP_TRACER_URL'
},
basnet: {
displayName: 'BASNet',
shortName: "BASNet",
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
apiUrlVar: 'REACT_APP_BASNET_URL'
},
deeplab: {
displayName: 'DeepLabV3',
shortName: "DeepLab",
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
apiUrlVar: 'REACT_APP_DEEPLAB_URL'
},
u2net_human_seg: {
displayName: 'U2Net Human',
shortName: "U2Net🧍",
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
apiUrlVar: 'REACT_APP_U2NET_HUMAN_SEG_URL'
},
ormbg: {
displayName: 'Open RMBG',
shortName: "ORMBG",
sourceUrl: 'https://huggingface.co/schirrmacher/ormbg',
apiUrlVar: 'REACT_APP_ORMBG_URL'
},
'isnet-general-use': {
displayName: 'ISNET-DIS',
shortName: "DIS",
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
apiUrlVar: 'REACT_APP_ISNET_GENERAL_URL'
},
'isnet-anime': {
displayName: 'ISNET-Anime',
shortName: "Anime",
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
apiUrlVar: 'REACT_APP_ISNET_ANIME_URL'
}
};
export default ModelsInfo;

View file

@ -0,0 +1,191 @@
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import MenuItem from '@mui/material/MenuItem';
import AdbIcon from '@mui/icons-material/Adb';
import { Deblur } from '@mui/icons-material';
import ModeNightIcon from '@mui/icons-material/ModeNight';
import LightModeIcon from '@mui/icons-material/LightMode';
import Brightness3Icon from '@mui/icons-material/Brightness3';
//const pages = ['Products', 'Pricing', 'Blog'];
const pages = ['About', 'Help'];
const pageUrls = {
About: 'https://fyrean.itch.io/',
//Help: '#',
};
const settings = ['Profile', 'Account', 'Dashboard', 'Logout'];
function ResponsiveAppBar({toggleTheme, darkMode}) {
const [anchorElNav, setAnchorElNav] = React.useState(null);
const [anchorElUser, setAnchorElUser] = React.useState(null);
const handleOpenNavMenu = (event) => {
setAnchorElNav(event.currentTarget);
};
const handleOpenUserMenu = (event) => {
setAnchorElUser(event.currentTarget);
};
const handleCloseNavMenu = () => {
setAnchorElNav(null);
};
const handleCloseUserMenu = () => {
setAnchorElUser(null);
};
const handlePageClick = (page) => {
if (!pageUrls[page])
return;
window.open(pageUrls[page], '_blank');
handleCloseNavMenu();
};
return (
<AppBar position="fixed" sx={{}} >
<Container maxWidth="xl" >
<Toolbar disableGutters variant="dense" sx={{
maxHeight:'20px',
minHeight:0
}}>
<Deblur sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
<Typography
variant="h6"
noWrap
component="a"
href="https://bgbye.fyrean.com"
sx={{
mr: 2,
display: { xs: 'none', md: 'flex' },
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.3rem',
color: 'inherit',
textDecoration: 'none',
}}
>
BGBye
</Typography>
{false&&<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none'} }}>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleOpenNavMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
sx={{
display: { xs: 'block', md: 'none'}
}}
>
{pages.map((page) => (
<MenuItem key={page} onClick={() => handlePageClick(page)}>
<Typography textAlign="center" variant="subtitle2">{page}</Typography>
</MenuItem>
))}
<MenuItem key='theme' onClick={toggleTheme} variant="contained">
{!darkMode ? <LightModeIcon /> : <ModeNightIcon />}
{!darkMode ? ' Light' : ' Dark'}
</MenuItem>
</Menu>
</Box>}
<Deblur sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
<Typography
variant="subtitle2"
noWrap
component="a"
href="#app-bar-with-responsive-menu"
sx={{
mr: 2,
display: { xs: 'flex', md: 'none' },
flexGrow: 0,
fontFamily: 'monospace',
fontWeight: 700,
letterSpacing: '.3rem',
color: 'inherit',
textDecoration: 'none',
}}
>
Matxinh
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'flex' } }}>
{pages.map((page) => (
<Button
key={page}
onClick={() => handlePageClick(page)}
sx={{ my: 2, color: 'white', display: 'block' }}
>
{page}
</Button>
))}
<IconButton onClick={toggleTheme} color={darkMode?'primary':'warning'} size="large">
{!darkMode ? <LightModeIcon fontSize="inherit" /> : <Brightness3Icon fontSize="inherit" />}
</IconButton>
</Box>
{false&&<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt="Remy Sharp" src="/static/images/avatar/2.jpg" />
</IconButton>
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
{settings.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))}
</Menu>
</Box>}
</Toolbar>
</Container>
</AppBar>
);
}
export default ResponsiveAppBar;

50
bgbye/src/index.css Normal file
View file

@ -0,0 +1,50 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.slider-example-focus {
/* #549ef7 #ffa658
The style below isn't necessary.
It has been added for the sake of smoothness.
transition: box-shadow 200ms ease-in-out;*/
box-shadow: 0px 0px 10px 5px #6464647a;
display: block;
--default-handle-shadow: 0px 0px 5px rgba(0, 0, 0, 1);
--divider-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
--divider-width:2;
--default-handle-width: 100px;
--divider-color: #549ef7;
--default-handle-color: #549ef7;
stroke-width: 3px;
;
}
.slider-example-focus:focus {
outline: none;
box-shadow: 0px 0px 10px 5px #6464647a;
}
.checkerboard {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
background-size: 20px 20px;
}
.magnifying-glass {
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
background-size: 20px 20px;
}

17
bgbye/src/index.js Normal file
View file

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
//import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
//reportWebVitals();

1
bgbye/src/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
bgbye/src/setupTests.js Normal file
View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';