Repository: voxel51/fiftyone

Issue:
[?] How to pass custom parameters to YOLO models in `apply_model()`
My aim is to use apply_model with a yolov11 model. I need to send some args to the model. Analyzing the document I see that kwargs can be used for this purpose.

```
**kwargs: optional model-specific keyword arguments passed through
                to the underlying inference implementation
```

My baseline is:
```
# Apply the model to the dataset
model = YOLO(
    "weights/best.pt"
)
dataset.apply_model(
    model,
    label_field="yolov11",
)
```

CASE1:
```
# Apply the model to the dataset
dataset.apply_model(
    model,
    label_field="yolov11",
    kwargs={"conf": 0.25, "iou": 0.1, "agnostic_nms": True, "imgsz": 512},
)
```

CASE2:
```
# Apply the model to the dataset
dataset.apply_model(
    model,
    label_field="yolov11",
    conf=0.25,
    iou=0.7,
    agnostic_nms=True,
    imgsz=512,
)
```

The two cases above result in the same label with the baseline showing that my arguments are not sent to the underlying model.
Can it be because here kwargs is not received but not used?
https://github.com/voxel51/fiftyone/blob/67bdf7e60a752b2686c2050d4f7ec16141fef988/fiftyone/core/models.py#L52

Discussion:
@Burhan-Q: The `conf` value can be passed directly to the `apply_model` method using the `confidence_thresh` argument. For everything specific to the Ultralytics model, I recommend using the following:

```py
import fiftyone as fo
from ultralytics import YOLO

dataset = fo.load_dataset("path/to/dataset")
model = YOLO("yolo11n.pt")

# Set model overrides
custom_cfg = {"iou": 0.1, "agnostic_nms": True, "imgsz": 512}
for k,v in custom_cfg.items():
    model.overrides[k] = v

dataset.apply_model(model, label_field="yolov11", confidence_thresh=0.25)
```

<details><summary>Reproducible example</summary>
<p>

```python
import fiftyone.zoo as foz
from fiftyone import ViewField as F

from ultralytics import YOLO

model = YOLO("yolo11n.pt")

ds = foz.load_zoo_dataset("quickstart")

cfg = {
    "classes":[0,2],  # limit inference to only "person" and "car" classes
    "imgsz":320, 
    "device":"cuda"  # only with GPU and PyTorch+CUDA installed
}
for k,v in cfg.items():
    model.overrides[k] = v

ds.apply_model(
    model,
    label_field="yolo11-predictions",
    confidence_thresh=0.25,
)

view = ds.filter_labels("yolo11-predictions", F("label") is not None)
len(view)
# >>> 84

# re-run without setting model overrides
model = YOLO("yolo11n.pt")  # load new model instance
ds.delete_sample_fields("yolo11-predictions")  # clear field

ds.apply_model(
    model,
    label_field="yolo11-predictions",
    confidence_thresh=0.25,
)
view = ds.filter_labels("yolo11-predictions", F("label") is not None)
len(view)
# >>> 199
```

If you re-run _without_ setting the `overrides` you'll find that the resulting view will have significantly more samples where the label value is not `None` (199 vs 84).

</p>
</details> 
@dangom13: I still don't think the `confidence_thresh` parameter is working for yolo models. It does not seem to have an impact. @brimoor: @dangom13 appears to be working for me:

```py
import fiftyone as fo
import fiftyone.zoo as foz
from ultralytics import YOLO

dataset = foz.load_zoo_dataset("quickstart", max_samples=10)
model = YOLO("yolo11n.pt")

dataset.apply_model(model, label_field="yolov11", confidence_thresh=0.9)

print(dataset.bounds("yolov11.detections.confidence"))
# (0.9024709463119507, 0.949521005153656)

dataset.apply_model(model, label_field="yolov11", confidence_thresh=0.5)

print(dataset.bounds("yolov11.detections.confidence"))
# (0.547297477722168, 0.949521005153656)
```
@oguz-hanoglu: Thank you for responding. I checked comprehensively and I can verify that both confidence_thresh and custom_cfg are working. 

For other users, it would be good to see this feature (model.overrides) in documentation (if it is and I am missing it, please ignore this comment) because this seems like a feature most users would need.@allenleetc: Hey @Burhan-Q @oguz-hanoglu 

Does the custom config work for a `conf` value that is less than 0.25? I just tried this and it doesn't seem to give the expected result

```
from ultralytics import YOLO

model = YOLO("yolo11n.pt")

ds = fo.load_dataset('demo')

custom_cfg = {"conf": 0.05}
for k,v in custom_cfg.items():
    model.overrides[k] = v

ds.apply_model(model, label_field="yolov11")
print(ds.min(F('yolov11.detections.confidence'))) # prints 0.2502

ds.apply_model(model, label_field="yolov11_2",confidence_thresh=0.05)
print(ds.min(F('yolov11_2.detections.confidence')))  # prints 0.2502
```@Burhan-Q: @allenleetc I found the same thing. It appears that `ultralytics` is not respecting the `conf` value for prediction when using the `model.overrides` which is likely due to the fact most users are going to pass the `conf` argument directly, which would take precedence. I'm going to open an `ultralytics` PR to propose fixing this but it's not clear to me why this would be the default behavior.

```python
import fiftyone as fo
import fiftyone.zoo as foz
from fiftyone import ViewField as F
from ultralytics import YOLO

dataset = foz.load_zoo_dataset("quickstart")

# using model overrides

model = YOLO("yolo11n.pt")
model.overrides
>>> {
    'task': 'detect', 
    'data': '/usr/src/ultralytics/ultralytics/cfg/datasets/coco.yaml', 
    'imgsz': 640, 
    'single_cls': False, 
    'model': 'yolo11n.pt'
}
model.overrides["conf"] = 0.05
model.overrides
>>> {
    'task': 'detect', 
    'data': '/usr/src/ultralytics/ultralytics/cfg/datasets/coco.yaml', 
    'imgsz': 640, 
    'single_cls': False, 
    'model': 'yolo11n.pt', 
    'conf': 0.05
}

dataset.apply_model(model, label_field="yolov11")
print(dataset.min(F('yolov11.detections.confidence')))
>>> 0.25003916025161743

model.overrides
>>> {
    'task': 'detect', 
    'data': '/usr/src/ultralytics/ultralytics/cfg/datasets/coco.yaml', 
    'imgsz': 640, 
    'single_cls': False, 
    'model': 'yolo11n.pt', 
    'conf': 0.05, 
    'device': device(type='cuda', index=0)  # overrides dict updated after model inference
}

# Using confidence_thresh argument

model = YOLO("yolo11n.pt")  # reinit model

dataset.apply_model(model, label_field="yolov11", confidence_thresh=0.05)
print(dataset.min(F('yolov11.detections.confidence')))
>>> 0.25003916025161743
```

Solution patch:
diff --git a/README.md b/README.md
index 1436b5b02..25df07841 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ data quality more efficiently than ever before 🤝
 
 If you're looking to scale to production-grade, collaborative, cloud-native
 enterprise workloads, check out
-**[FiftyOne Teams](http://voxel51.com/enterprise)** 🚀
+**[FiftyOne Enterprise](http://voxel51.com/enterprise)** 🚀
 
 ![------------------------------------------------------------------](https://github.com/user-attachments/assets/fb0573d0-bb56-40ff-9ae1-a5e8f62f5f42)
 
@@ -455,8 +455,8 @@ https://github.com/user-attachments/assets/c7ed496d-0cf7-45d6-9853-e349f1abd6f8
 
 ## <img src="https://user-images.githubusercontent.com/25985824/106288517-2422e000-6216-11eb-871d-26ad2e7b1e59.png" height="20px"> &nbsp; additional resources &nbsp; 🚁
 
-| [FiftyOne Teams](https://voxel51.com/enterprise) | [VoxelGPT](https://github.com/voxel51/voxelgpt) | [Plugins](https://voxel51.com/plugins) | [Vector Search](https://voxel51.com/blog/the-computer-vision-interface-for-vector-search) | [Dataset Zoo](https://docs.voxel51.com/dataset_zoo/index.html) | [Model Zoo](https://docs.voxel51.com/model_zoo/index.html) | [FiftyOne Brain](https://docs.voxel51.com/brain.html) |
-| ------------------------------------------------ | ----------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- |
+| [FiftyOne Enterprise](https://voxel51.com/enterprise) | [VoxelGPT](https://github.com/voxel51/voxelgpt) | [Plugins](https://voxel51.com/plugins) | [Vector Search](https://voxel51.com/blog/the-computer-vision-interface-for-vector-search) | [Dataset Zoo](https://docs.voxel51.com/dataset_zoo/index.html) | [Model Zoo](https://docs.voxel51.com/model_zoo/index.html) | [FiftyOne Brain](https://docs.voxel51.com/brain.html) |
+| ----------------------------------------------------- | ----------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- |
 
 </div>
 
@@ -478,11 +478,11 @@ Full documentation for FiftyOne is available at
 
 <div id='fiftyone-teams'/>
 
-## <img src="https://user-images.githubusercontent.com/25985824/106288517-2422e000-6216-11eb-871d-26ad2e7b1e59.png" height="20px"> &nbsp; FiftyOne Teams &nbsp; 🏎️
+## <img src="https://user-images.githubusercontent.com/25985824/106288517-2422e000-6216-11eb-871d-26ad2e7b1e59.png" height="20px"> &nbsp; FiftyOne Enterprise &nbsp; 🏎️
 
 Want to securely collaborate on billions of samples in the cloud and connect to
 your compute resources to automate your workflows? Check out
-[FiftyOne Teams](https://voxel51.com/enterprise).
+[FiftyOne Enterprise](https://voxel51.com/enterprise).
 
 ![------------------------------------------------------------------](https://github.com/user-attachments/assets/fb0573d0-bb56-40ff-9ae1-a5e8f62f5f42)
 
diff --git a/app/packages/app/src/components/Teams.tsx b/app/packages/app/src/components/Teams.tsx
index b8deba657..29410c441 100644
--- a/app/packages/app/src/components/Teams.tsx
+++ b/app/packages/app/src/components/Teams.tsx
@@ -1,12 +1,325 @@
-import { Button, ExternalLink } from "@fiftyone/components";
-import React from "react";
+import { Button, ExternalLink, useTheme } from "@fiftyone/components";
+import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
+import {
+  Box,
+  Button as MuiButton,
+  Popover,
+  Stack,
+  Typography,
+  useColorScheme,
+} from "@mui/material";
+import React, { useCallback, useEffect, useState } from "react";
+import styled, { css, keyframes } from "styled-components";
+
+const ENTERPRISE_TOOLTIP_LS = "fiftyone-enterprise-tooltip-seen";
+const ENTERPRISE_BUTTON_ID = "fo-cta-enterprise-button";
+
+const DARK_BG_COLOR = "#333333";
+const LIGHT_BG_COLOR = "#FFFFFF";
+
+const GRADIENT_START_COLOR = "#FF6D04";
+const GRADIENT_END_COLOR = "#B681FF";
+
+// subtle pulse animation for the sparkles icon
+const pulse = keyframes`
+  0% {
+    transform: scale(1);
+    opacity: 1;
+  }
+  50% {
+    transform: scale(1.1);
+    opacity: 0.9;
+  }
+  100% {
+    transform: scale(1);
+    opacity: 1;
+  }
+`;
+
+// styles that will be applied to the icon container
+const pulseAnimation = css`
+  animation: ${pulse} 1.5s ease-in-out infinite;
+`;
+
+const IconContainer = styled.div`
+  display: flex;
+  align-items: center;
+  transition: all 0.3s ease;
+`;
+
+// https://stackoverflow.com/questions/65282139/adding-a-linear-gradient-to-material-ui-icon
+const GradientAutoAwesomeIcon = () => (
+  <>
+    <svg width={0} height={0} aria-label="Gradient" aria-labelledby="gradient">
+      <title>Gradient</title>
+      <defs>
+        <linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%">
+          <stop
+            offset="0%"
+            style={{ stopColor: GRADIENT_START_COLOR, stopOpacity: 1 }}
+          />
+          <stop
+            offset="100%"
+            style={{ stopColor: GRADIENT_END_COLOR, stopOpacity: 1 }}
+          />
+        </linearGradient>
+      </defs>
+    </svg>
+    <IconContainer className="fo-teams-cta-pulse-animation">
+      <AutoAwesomeIcon
+        sx={{
+          fontSize: { xs: 16, sm: 20 },
+          mr: 1,
+          fill: "url(#gradient1)",
+        }}
+      />
+    </IconContainer>
+  </>
+);
+
+const ButtonContainer = styled.div<{ bgColor: string }>`
+  background-color: ${({ bgColor }) => bgColor};
+  border-radius: 16px;
+
+  &:hover {
+    background-color: transparent;
+  }
+`;
+
+const StyledExternalLink = styled(ExternalLink)`
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: none;
+  }
+`;
+
+const BaseEnterpriseButton = styled(Button)<{
+  borderColor: string;
+  isLightMode?: boolean;
+}>`
+  background: linear-gradient(45deg, #ff6d04 0%, #b681ff 100%);
+  background-clip: text;
+  -webkit-background-clip: text;
+  text-fill-color: transparent;
+  -webkit-text-fill-color: transparent;
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  padding: 6px 12px;
+  border-radius: 16px;
+  font-weight: 500;
+  text-transform: none;
+  transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+  text-decoration: none;
+  font-size: 16px;
+  position: relative;
+  overflow: hidden;
+  border: 1px solid ${({ borderColor }) => borderColor};
+  outline: none;
+  box-shadow: none;
+
+  @media (max-width: 767px) {
+    font-size: 14px;
+    padding: 4px 10px;
+  }
+
+  &:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: ${({ isLightMode }) => (isLightMode ? "150%" : "100%")};
+    height: 100%;
+    background: linear-gradient(
+      90deg,
+      rgba(255, 255, 255, 0) 0%,
+      rgba(255, 255, 255, ${({ isLightMode }) => (isLightMode ? "0.3" : "0.2")})
+        50%,
+      rgba(255, 255, 255, 0) 100%
+    );
+    transition: all ${({ isLightMode }) => (isLightMode ? "0.8s" : "0.6s")} ease;
+    z-index: 1;
+  }
+
+  &:hover,
+  &:focus,
+  &:active {
+    transform: scale(1.03);
+    text-decoration: none;
+    border: 1px solid ${({ borderColor }) => borderColor} !important;
+    outline: none;
+    box-shadow: none;
+
+    background: linear-gradient(45deg, #ff6d04 0%, #b681ff 100%) !important;
+    background-clip: text !important;
+    -webkit-background-clip: text !important;
+    text-fill-color: transparent !important;
+    -webkit-text-fill-color: transparent !important;
+
+    &:before {
+      left: 100%;
+      background: linear-gradient(
+        90deg,
+        rgba(255, 255, 255, 0) 0%,
+        rgba(
+            255,
+            255,
+            255,
+            ${({ isLightMode }) => (isLightMode ? "0.6" : "0.2")}
+          )
+          50%,
+        rgba(255, 255, 255, 0) 100%
+      );
+    }
+
+    .fo-teams-cta-pulse-animation {
+      ${pulseAnimation}
+    }
+  }
+`;
+
+const PopoverContent = styled(Box)`
+  padding: 16px;
+  width: 310px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+`;
+
+const PopoverHeading = styled(Typography)`
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 600;
+  margin-bottom: 12px;
+`;
+
+const PopoverBody = styled(Typography)`
+  position: relative;
+  color: var(--fo-palette-text-secondary);
+  font-size: 15px !important;
+`;
+
+const PopoverFooter = styled(Stack)`
+  margin-top: 16px;
+`;
+
+export default function Teams({
+  disablePopover = false,
+}: { disablePopover?: boolean }) {
+  const [showPopover, setShowPopover] = useState(false);
+
+  const { mode } = useColorScheme();
+  const theme = useTheme();
+
+  const bgColor = mode === "light" ? LIGHT_BG_COLOR : DARK_BG_COLOR;
+
+  useEffect(() => {
+    const hasSeenTooltip = window.localStorage.getItem(ENTERPRISE_TOOLTIP_LS);
+
+    // don't show intro popoverif we're in playwright
+    const isPlaywright = window["IS_PLAYWRIGHT"];
+    
+    if (!hasSeenTooltip && !isPlaywright) {
+      setShowPopover(true);
+    }
+  }, []);
+
+  const markTooltipSeen = useCallback(() => {
+    localStorage.setItem(ENTERPRISE_TOOLTIP_LS, "true");
+  }, []);
+
+  const handlePopoverClose = useCallback(() => {
+    markTooltipSeen();
+    setShowPopover(false);
+  }, [markTooltipSeen]);
+
+  const handleExplore = useCallback(() => {
+    markTooltipSeen();
+    setShowPopover(false);
+    window.open(
+      "https://voxel51.com/why-upgrade?utm_source=FiftyOneApp",
+      "_blank"
+    );
+  }, [markTooltipSeen]);
 
-export default function Teams() {
   return (
-    <ExternalLink
-      href={"https://voxel51.com/book-a-demo/?utm_source=FiftyOneApp"}
-    >
-      <Button>Have a Team?</Button>
-    </ExternalLink>
+    <>
+      <ButtonContainer bgColor={mode === "light" ? "transparent" : bgColor}>
+        <StyledExternalLink href="https://voxel51.com/why-upgrade?utm_source=FiftyOneApp">
+          <BaseEnterpriseButton
+            borderColor={mode === "dark" ? DARK_BG_COLOR : theme.divider}
+            isLightMode={mode === "light"}
+            id={ENTERPRISE_BUTTON_ID}
+          >
+            <GradientAutoAwesomeIcon />
+            Explore Enterprise
+          </BaseEnterpriseButton>
+        </StyledExternalLink>
+      </ButtonContainer>
+
+      {showPopover && !disablePopover && (
+        <Popover
+          open
+          anchorEl={document.getElementById(ENTERPRISE_BUTTON_ID)}
+          onClose={handlePopoverClose}
+          anchorOrigin={{
+            vertical: "bottom",
+            horizontal: "center",
+          }}
+          transformOrigin={{
+            vertical: -12,
+            horizontal: "center",
+          }}
+          elevation={3}
+        >
+          <PopoverContent
+            style={{
+              backgroundColor:
+                mode === "light" ? LIGHT_BG_COLOR : DARK_BG_COLOR,
+            }}
+          >
+            <PopoverHeading variant="h6">
+              <GradientAutoAwesomeIcon />
+              <Typography variant="h6" letterSpacing={0.3}>
+                Accelerate your workflow
+              </Typography>
+            </PopoverHeading>
+
+            <PopoverBody variant="body2">
+              With FiftyOne Enterprise you can connect to your data lake,
+              automate your data curation and model analysis tasks, securely
+              collaborate with your team, and more.
+            </PopoverBody>
+
+            <PopoverFooter direction="row" spacing={2}>
+              <MuiButton
+                variant="contained"
+                onClick={handleExplore}
+                size="large"
+                sx={{
+                  boxShadow: "none",
+                }}
+              >
+                Explore Enterprise
+              </MuiButton>
+              <MuiButton
+                variant="outlined"
+                color="secondary"
+                onClick={handlePopoverClose}
+                size="large"
+                sx={{
+                  boxShadow: "none",
+                }}
+              >
+                Dismiss
+              </MuiButton>
+            </PopoverFooter>
+          </PopoverContent>
+        </Popover>
+      )}
+    </>
   );
 }
diff --git a/app/packages/components/src/components/Teams/Teams.tsx b/app/packages/components/src/components/Teams/Teams.tsx
deleted file mode 100644
index ee502cd77..000000000
--- a/app/packages/components/src/components/Teams/Teams.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import React, { useState } from "react";
-import { Close } from "@mui/icons-material";
-import { useRecoilState } from "recoil";
-
-import { getFetchFunction } from "@fiftyone/utilities";
-
-const Teams = () => {
-  const [formState, setFormState] = useState({
-    email: "",
-    firstname: "",
-    lastname: "",
-    company: "",
-    role: "",
-    discover: "",
-  });
-  const [submitText, setSubmitText] = useState("Submit");
-  const [teams, setTeams] = useRecoilState(atoms.teams);
-  const portalId = 4972700;
-  const formId = "bca87445-10be-424c-9c07-e2b16770caf6";
-  const postUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formId}`;
-
-  const setFormValue = (name) => (e) =>
-    setFormState({
-      ...formState,
-      [name]: e.target.value,
-    });
-  const disabled =
-    !(
-      formState.email?.length &&
-      formState.firstname?.length &&
-      formState.lastname?.length
-    ) || teams.submitted;
-  const submit = () => {
-    if (disabled) {
-      return;
-    }
-    setSubmitText("Submitting...");
-    const headers = new Headers();
-    headers.append("Content-Type", "application/json");
-    const finalize = () => {
-      setSubmitText("Submitted. Thank you!");
-      setTeams((cur) => ({ ...cur, submitted: true }));
-      getFetchFunction()("POST", "/teams?submitted=true", {});
-      setTimeout(() => setTeams((cur) => ({ ...cur, open: false })), 2000);
-    };
-
-    fetch(postUrl, {
-      method: "post",
-      headers,
-      mode: "cors",
-      body: JSON.stringify({
-        submittedAt: Date.now(),
-        fields: [
-          {
-            name: "firstname",
-            value: formState.firstname,
-          },
-          {
-            name: "lastname",
-            value: formState.lastname,
-          },
-          {
-            name: "email",
-            value: formState.email,
-          },
-          {
-            name: "company",
-            value: formState.company,
-          },
-          {
-            name: "role",
-            value: formState.role,
-          },
-          {
-            name: "app_how_did_you_hear_about_us",
-            value: formState.discover,
-          },
-        ],
-        context: { pageName: "FiftyOne App" },
-      }),
-    })
-      .then((response) => {
-        if (response.status !== 200) {
-          throw new Error("Failed submission");
-        }
-        return response.json();
-      })
-      .then(() => {
-        finalize();
-      })
-      .catch((e) => {
-        setSubmitText("Something went wrong");
-      });
-  };
-  return (
-    <ContentDiv>
-      <Header>
-        <MessageTitle>Get FiftyOne for your team</MessageTitle>
-        <Button onClick={(e) => setTeams((cur) => ({ ...cur, open: false }))}>
-          <Close />
-        </Button>
-      </Header>
-      <Input
-        key="firstname"
-        placeholder={"First name*"}
-        value={formState.firstname ?? ""}
-        maxLength={40}
-        onChange={setFormValue("firstname")}
-      />
-      <Input
-        key="lastname"
-        placeholder={"Last name*"}
-        value={formState.lastname ?? ""}
-        maxLength={40}
-        onChange={setFormValue("lastname")}
-      />
-      <Input
-        key="email"
-        placeholder={"Email*"}
-        type="email"
-        value={formState.email ?? ""}
-        onChange={setFormValue("email")}
-      />
-      <Input
-        key="company"
-        placeholder={"Company"}
-        value={formState.company ?? ""}
-        maxLength={100}
-        onChange={setFormValue("company")}
-      />
-      <Input
-        key="role"
-        placeholder={"Role"}
-        value={formState.role ?? ""}
-        maxLength={100}
-        onChange={setFormValue("role")}
-      />
-      <Input
-        key="discover"
-        placeholder={"How did you hear about FiftyOne?"}
-        value={formState.discover ?? ""}
-        maxLength={100}
-        onChange={setFormValue("discover")}
-      />
-      <Button
-        key="submit"
-        onClick={submit}
-        style={{
-          marginBottom: "1rem",
-        }}
-        className={disabled ? "disabled" : ""}
-      >
-        {submitText}
-      </Button>
-    </ContentDiv>
-  );
-};
-
-export default Teams;
diff --git a/app/packages/components/src/components/Teams/index.ts b/app/packages/components/src/components/Teams/index.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/packages/core/src/components/Actions/Popout.tsx b/app/packages/core/src/components/Actions/Popout.tsx
index 32bb6edce..503ccc88f 100644
--- a/app/packages/core/src/components/Actions/Popout.tsx
+++ b/app/packages/core/src/components/Actions/Popout.tsx
@@ -71,6 +71,6 @@ type PopoutPropsType = {
   style?: React.CSSProperties;
   modal?: boolean;
   fixed?: boolean;
-  anchorRef?: RefObject<HTMLElement>;
+  anchorRef?: RefObject<HTMLElement> | null;
   testId?: string;
 };
diff --git a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/index.tsx b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/index.tsx
index a915d6348..865d12737 100644
--- a/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/index.tsx
+++ b/app/packages/core/src/plugins/SchemaIO/components/NativeModelEvaluationView/index.tsx
@@ -115,7 +115,7 @@ export default function NativeModelEvaluationView(props) {
         (showEmptyOverview || showCTA ? (
           <PanelCTA
             label="Create your first model evaluation"
-            demoLabel="Upgrade to FiftyOne Teams to Evaluate Models"
+            demoLabel="Upgrade to FiftyOne Enterprise to Evaluate Models"
             description="Analyze and improve models collaboratively with your team"
             docLink="https://docs.voxel51.com/user_guide/evaluation.html"
             docCaption="Learn how to evaluate models via code."
diff --git a/app/packages/embeddings/src/EmbeddingsCTA.tsx b/app/packages/embeddings/src/EmbeddingsCTA.tsx
index e5af37d79..8b7483499 100644
--- a/app/packages/embeddings/src/EmbeddingsCTA.tsx
+++ b/app/packages/embeddings/src/EmbeddingsCTA.tsx
@@ -16,7 +16,7 @@ export default function EmbeddingsCTA(props: EmbeddingsCTAProps) {
   return (
     <PanelCTA
       label="Embeddings help you explore and understand your dataset"
-      demoLabel="Upgrade to FiftyOne Teams to Create Embeddings"
+      demoLabel="Upgrade to FiftyOne Enterprise to Create Embeddings"
       description="You can compute and visualize embeddings for your dataset using a selection of pre-trained models or your own embeddings"
       docLink="https://docs.voxel51.com/user_guide/app.html#embeddings-panel"
       docCaption="Learn how to create embeddings visualizations via code."
diff --git a/docs/source/integrations/ultralytics.rst b/docs/source/integrations/ultralytics.rst
index dd01ddd16..9d4cd0f5a 100644
--- a/docs/source/integrations/ultralytics.rst
+++ b/docs/source/integrations/ultralytics.rst
@@ -150,14 +150,9 @@ You can also load any of these models directly from the
     # model_name = "yolo11x-coco-torch"
     # model_name = "rtdetr-l-coco-torch"
 
-    model = foz.load_zoo_model(
-        model_name,
-        label_field="boxes", 
-        confidence_thresh=0.5, 
-        iou_thresh=0.5,
-    )
+    model = foz.load_zoo_model(model_name)
 
-    dataset.apply_model(model)
+    dataset.apply_model(model, label_field="boxes", confidence_thresh=0.5)
 
     session = fo.launch_app(dataset)
 
@@ -242,9 +237,9 @@ You can also load YOLOv8, YOLOv9, and YOLO11 segmentation models from the
     # model_name = "yolo11x-seg-coco-torch"
     
 
-    model = foz.load_zoo_model(model_name, label_field="yolo_seg")
+    model = foz.load_zoo_model(model_name)
 
-    dataset.apply_model(model)
+    dataset.apply_model(model, label_field="yolo_seg")
 
     session = fo.launch_app(dataset)
 
@@ -369,29 +364,29 @@ that you can set the classes that the model should detect:
 
     from ultralytics import YOLO
 
-    ## Load dataset
+    # Load dataset
     dataset = foz.load_zoo_dataset(
         "voc-2007", split="validation", max_samples=100
     )
     dataset.select_fields().keep_fields()
 
-    ## Load model
+    # Load model
     model = YOLO("yolov8l-world.pt")
     # model = YOLO("yolov8s-world.pt")
     # model =  YOLO("yolov8m-world.pt")
     # model =  YOLO("yolov8x-world.pt")
 
-    ## Set open vocabulary classes
+    # Set open vocabulary classes
     model.set_classes(
         ["plant", "window", "keyboard", "human baby", "computer monitor"]
     )
 
     label_field = "yolo_world_detections"
 
-    ## Apply model
+    # Apply model
     dataset.apply_model(model, label_field=label_field)
 
-    ## Visualize the detection patches
+    # Visualize the detection patches
     patches = dataset.to_patches(label_field)
     session = fo.launch_app(patches)
 
diff --git a/fiftyone/core/collections.py b/fiftyone/core/collections.py
index 1f6508577..27a6a26c0 100644
--- a/fiftyone/core/collections.py
+++ b/fiftyone/core/collections.py
@@ -1425,7 +1425,7 @@ class SampleCollection(object):
                 return None, None, read_only
 
             resolved_keys.append(field.db_field or field.name)
-            read_only = field.read_only
+            read_only = getattr(field, "read_only", False)
             last_key = idx == len(keys) - 1
 
             if last_key and not leaf:
diff --git a/fiftyone/core/metadata.py b/fiftyone/core/metadata.py
index dd75a9e16..000e37269 100644
--- a/fiftyone/core/metadata.py
+++ b/fiftyone/core/metadata.py
@@ -459,8 +459,8 @@ def get_image_info(f):
     img = Image.open(f)
 
     # Flip the dimensions if image metadata requires us to. PIL.Image doesn't
-    #   handle by default.
-    if _image_has_flipped_dimensions(img):
+    #   handle by default. Image flipping only efficiently supported on JPEG.
+    if img.format == "JPEG" and _image_has_flipped_dimensions(img):
         width, height = img.height, img.width
     else:
         width, height = img.width, img.height
@@ -555,13 +555,12 @@ def _do_compute_metadata(args):
 def _compute_sample_metadata(
     filepath, media_type, skip_failures=False, cache=None
 ):
-    if not skip_failures:
-        return _get_metadata(filepath, media_type, cache=cache)
-
     try:
         return _get_metadata(filepath, media_type, cache=cache)
     except:
-        return None
+        if skip_failures:
+            return None
+        raise
 
 
 def _get_metadata(filepath, media_type, cache=None):
diff --git a/fiftyone/core/odm/mixins.py b/fiftyone/core/odm/mixins.py
index 1ce24be7b..80e6c3b79 100644
--- a/fiftyone/core/odm/mixins.py
+++ b/fiftyone/core/odm/mixins.py
@@ -5,6 +5,7 @@ Mixins and helpers for dataset backing documents.
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
+
 from collections import OrderedDict
 from datetime import datetime
 import itertools
@@ -305,7 +306,7 @@ class DatasetMixin(object):
 
         for path in new_schema.keys():
             _, _, _, root_doc = cls._parse_path(path)
-            if root_doc is not None and root_doc.read_only:
+            if root_doc is not None and getattr(root_doc, "read_only", False):
                 root = path.rsplit(".", 1)[0]
                 raise ValueError("Cannot edit read-only field '%s'" % root)
 
@@ -318,7 +319,10 @@ class DatasetMixin(object):
         # Silently skip updating metadata of any read-only fields
         for path in list(new_metadata.keys()):
             field = cls._get_field(path, allow_missing=True)
-            if field is not None and field.read_only:
+            if field is not None and (
+                not isinstance(field, fof.Field)
+                or getattr(field, "read_only", False)
+            ):
                 del new_metadata[path]
 
         for path, field in new_schema.items():
@@ -517,7 +521,7 @@ class DatasetMixin(object):
                     % (cls._doc_name().lower(), path)
                 )
 
-            if field is not None and field.read_only:
+            if field is not None and getattr(field, "read_only", False):
                 raise ValueError(
                     "Cannot rename read-only %s field '%s'"
                     % (cls._doc_name().lower(), path)
@@ -684,7 +688,7 @@ class DatasetMixin(object):
                     "%s field '%s' does not exist" % (cls._doc_name(), path)
                 )
 
-            if field is not None and field.read_only:
+            if field is not None and getattr(field, "read_only", False):
                 raise ValueError(
                     "Cannot rename read-only %s field '%s'"
                     % (cls._doc_name().lower(), path)
@@ -753,7 +757,7 @@ class DatasetMixin(object):
                 )
                 continue
 
-            if field is not None and field.read_only:
+            if field is not None and getattr(field, "read_only", False):
                 raise ValueError(
                     "Cannot delete read-only %s field '%s'"
                     % (cls._doc_name().lower(), path)
@@ -862,7 +866,7 @@ class DatasetMixin(object):
                 )
                 continue
 
-            if field is not None and field.read_only:
+            if field is not None and getattr(field, "read_only", False):
                 fou.handle_error(
                     ValueError(
                         "Cannot remove read-only %s field '%s'"


Test patch:
diff --git a/e2e-pw/src/oss/specs/regression-tests/index-page.spec.ts b/e2e-pw/src/oss/specs/regression-tests/index-page.spec.ts
index 18680d47e..16611a2ac 100644
--- a/e2e-pw/src/oss/specs/regression-tests/index-page.spec.ts
+++ b/e2e-pw/src/oss/specs/regression-tests/index-page.spec.ts
@@ -28,6 +28,13 @@ test.beforeAll(async ({ fiftyoneLoader, foWebServer }) => {
 
 test.describe.serial("index page", () => {
   test("index page", async ({ pagePom, page }) => {
+    await page.addInitScript(() => {
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore injecting IS_PLAYWRIGHT into window so that
+      // we can disable 1) analytics, and 2) QA performance toast banners
+      window.IS_PLAYWRIGHT = true;
+    });
+
     await pagePom.loadDataset();
     await pagePom.assert.verifyPage("index");
     await pagePom.assert.verifyPathname("/");
diff --git a/tests/unittests/video_tests.py b/tests/unittests/video_tests.py
index 5139a753e..23f528b85 100644
--- a/tests/unittests/video_tests.py
+++ b/tests/unittests/video_tests.py
@@ -5,6 +5,7 @@ FiftyOne video-related unit tests.
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
+
 from copy import deepcopy
 from datetime import date, datetime
 
@@ -2397,7 +2398,13 @@ class VideoTests(unittest.TestCase):
         sample.frames[1] = fo.Frame(filepath="image.jpg")
 
         dataset = fo.Dataset()
+
         dataset.add_sample(sample)
+        dataset.add_frame_field(
+            "metadata",
+            fo.EmbeddedDocumentField,
+            embedded_doc_type=fo.ImageMetadata,
+        )
 
         frames = dataset.to_frames()
         view = frames.select_fields()