Skip to content

Commit 820da06

Browse files
authored
Merge pull request #213 from roboflow/feature/notebook
Built In Jupyter Notebook
2 parents cb044a0 + 6658654 commit 820da06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+900
-84
lines changed

docker/dockerfiles/Dockerfile.onnx.cpu

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ RUN pip3 install --upgrade pip && pip3 install \
3434
-r requirements.gaze.txt \
3535
-r requirements.doctr.txt \
3636
-r requirements.groundingdino.txt \
37+
jupyterlab \
3738
wheel>=0.38.0 \
3839
setuptools>=65.5.1 \
3940
--upgrade \
@@ -42,8 +43,15 @@ RUN pip3 install --upgrade pip && pip3 install \
4243
FROM scratch
4344
COPY --from=base / /
4445

45-
WORKDIR /app
46+
WORKDIR /build
47+
COPY . .
48+
RUN make create_wheels
49+
RUN pip3 install dist/inference_core*.whl dist/inference_cpu*.whl dist/inference_sdk*.whl
50+
51+
WORKDIR /notebooks
52+
COPY examples/notebooks .
4653

54+
WORKDIR /app
4755
COPY inference inference
4856
COPY docker/config/cpu_http.py cpu_http.py
4957

docker/dockerfiles/Dockerfile.onnx.gpu

+10
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,22 @@ RUN pip3 install --upgrade pip && pip3 install \
3535
-r requirements.groundingdino.txt \
3636
-r requirements.doctr.txt \
3737
-r requirements.cogvlm.txt \
38+
jupyterlab \
3839
--upgrade \
3940
&& rm -rf ~/.cache/pip
4041

4142
FROM scratch
4243
COPY --from=base / /
4344

45+
WORKDIR /build
46+
COPY . .
47+
RUN ln -s /usr/bin/python3 /usr/bin/python
48+
RUN /bin/make create_wheels
49+
RUN pip3 install dist/inference_core*.whl dist/inference_gpu*.whl dist/inference_sdk*.whl
50+
51+
WORKDIR /notebooks
52+
COPY examples/notebooks .
53+
4454
WORKDIR /app/
4555
COPY inference inference
4656
COPY docker/config/gpu_http.py gpu_http.py

docker/dockerfiles/Dockerfile.onnx.jetson.4.5.0

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ RUN python3.8 -m pip install --upgrade pip && python3.8 -m pip install \
3838
-r requirements.http.txt \
3939
-r requirements.doctr.txt \
4040
-r requirements.groundingdino.txt \
41+
jupyterlab \
4142
--upgrade \
4243
&& rm -rf ~/.cache/pip
4344

docker/dockerfiles/Dockerfile.onnx.jetson.4.6.1

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ RUN python3.8 -m pip install --upgrade pip && python3.8 -m pip install \
3838
-r requirements.http.txt \
3939
-r requirements.doctr.txt \
4040
-r requirements.groundingdino.txt \
41+
jupyterlab \
4142
--upgrade \
4243
&& rm -rf ~/.cache/pip
4344

docker/dockerfiles/Dockerfile.onnx.jetson.5.1.1

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ RUN pip3 install --upgrade pip && pip3 install \
3636
-r requirements.http.txt \
3737
-r requirements.doctr.txt \
3838
-r requirements.groundingdino.txt \
39+
jupyterlab \
3940
--upgrade \
4041
&& rm -rf ~/.cache/pip
4142

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "83db9682-cfc4-4cd0-889f-c8747c4033b3",
6+
"metadata": {},
7+
"source": [
8+
"# Inference Pipeline\n",
9+
"\n",
10+
"Inference Pipelines are a great way to process video streams with Inference. You can configure different sources that include streams from local devices, RTSP streams, and local video files. You can also configure different sinks that include UDP streaming of results, render of results, and custom callbacks to run your own logic after each new set of predictions is available. "
11+
]
12+
},
13+
{
14+
"cell_type": "markdown",
15+
"id": "4ec4136f-53e9-4c8c-9217-a2c533d498ae",
16+
"metadata": {},
17+
"source": [
18+
"### Roboflow API Key\n",
19+
"\n",
20+
"To load models with `inference`, you'll need a Roboflow API Key. Find instructions for retrieving your API key [here](https://docs.roboflow.com/api-reference/authentication). The utility function below attempts to load your Roboflow API key from your enviornment. If it isn't found, it then prompts you to input it. To avoid needing to input your API key for each example, you can configure your Roboflow API key in your environment via the variable `ROBOFLOW_API_KEY`."
21+
]
22+
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": null,
26+
"id": "af3aad40-d41b-4bc1-ade8-dac052951257",
27+
"metadata": {},
28+
"outputs": [],
29+
"source": [
30+
"from utils import get_roboflow_api_key\n",
31+
"\n",
32+
"api_key = get_roboflow_api_key()"
33+
]
34+
},
35+
{
36+
"cell_type": "markdown",
37+
"id": "86f3f805-f628-4e94-91ac-3b2f44bebdc0",
38+
"metadata": {},
39+
"source": [
40+
"### Inference Pipeline Example\n",
41+
"\n",
42+
"In this example we create a new InferencePipeline. We pass the model ID, the video reference, and a method to render our results. Out pipeline does the rest!"
43+
]
44+
},
45+
{
46+
"cell_type": "code",
47+
"execution_count": null,
48+
"id": "58dd049c-dcc6-4d0b-85ad-e6d1c0ba805b",
49+
"metadata": {},
50+
"outputs": [],
51+
"source": [
52+
"from functools import partial\n",
53+
"\n",
54+
"import numpy as np\n",
55+
"from matplotlib import pyplot as plt\n",
56+
"from IPython import display\n",
57+
"\n",
58+
"from inference.core.interfaces.stream.inference_pipeline import InferencePipeline\n",
59+
"from inference.core.interfaces.stream.sinks import render_boxes\n",
60+
"\n",
61+
"# Define source video\n",
62+
"video_url = \"https://storage.googleapis.com/com-roboflow-marketing/football-video.mp4\"\n",
63+
"\n",
64+
"# Prepare to plot results\n",
65+
"\n",
66+
"fig, ax = plt.subplots()\n",
67+
"frame_placeholder = np.zeros((480, 640, 3), dtype=np.uint8) # Adjust the dimensions to match your frame size\n",
68+
"image_display = ax.imshow(frame_placeholder)\n",
69+
"\n",
70+
"# Define our plotting function\n",
71+
"def update_plot(new_frame):\n",
72+
" # Update the image displayed\n",
73+
" image_display.set_data(new_frame)\n",
74+
" # Redraw the canvas immediately\n",
75+
" display.display(plt.gcf())\n",
76+
" display.clear_output(wait=True)\n",
77+
"\n",
78+
"# Define our pipeline's sink\n",
79+
"render = partial(render_boxes, on_frame_rendered=update_plot)\n",
80+
"\n",
81+
"# Instantiate the pipeline\n",
82+
"pipeline = InferencePipeline.init(\n",
83+
" model_id=\"soccer-players-5fuqs/1\",\n",
84+
" video_reference=video_url,\n",
85+
" on_prediction=render,\n",
86+
" api_key=api_key,\n",
87+
")\n",
88+
"\n",
89+
"# Start the pipeline\n",
90+
"pipeline.start()\n",
91+
"pipeline.join()"
92+
]
93+
},
94+
{
95+
"cell_type": "code",
96+
"execution_count": null,
97+
"id": "07762936-ff33-46c0-a4a2-0a8e729053d1",
98+
"metadata": {},
99+
"outputs": [],
100+
"source": []
101+
}
102+
],
103+
"metadata": {
104+
"kernelspec": {
105+
"display_name": "Python 3 (ipykernel)",
106+
"language": "python",
107+
"name": "python3"
108+
},
109+
"language_info": {
110+
"codemirror_mode": {
111+
"name": "ipython",
112+
"version": 3
113+
},
114+
"file_extension": ".py",
115+
"mimetype": "text/x-python",
116+
"name": "python",
117+
"nbconvert_exporter": "python",
118+
"pygments_lexer": "ipython3",
119+
"version": "3.9.18"
120+
}
121+
},
122+
"nbformat": 4,
123+
"nbformat_minor": 5
124+
}

examples/notebooks/inference_sdk.ipynb

+193
Large diffs are not rendered by default.

examples/notebooks/quickstart.ipynb

+212
Large diffs are not rendered by default.

examples/notebooks/utils.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import getpass
2+
import requests
3+
4+
import cv2
5+
import numpy as np
6+
7+
from inference.core.env import API_KEY
8+
9+
def get_roboflow_api_key():
10+
if API_KEY is None:
11+
api_key = getpass.getpass("Roboflow API Key:")
12+
else:
13+
api_key = API_KEY
14+
return api_key
15+
16+
def load_image_from_url(url):
17+
# Send a GET request to the URL
18+
response = requests.get(url)
19+
20+
# Ensure that the request was successful
21+
if response.status_code == 200:
22+
# Convert the response content into a numpy array
23+
image_array = np.asarray(bytearray(response.content), dtype=np.uint8)
24+
25+
# Decode the image array into an OpenCV image
26+
image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
27+
28+
return image
29+
else:
30+
print(f"Failed to retrieve the image. HTTP status code: {response.status_code}")
31+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pydantic import BaseModel, Field, ValidationError
2+
3+
4+
class NotebookStartResponse(BaseModel):
5+
"""Response model for notebook start request"""
6+
7+
success: str = Field(..., description="Status of the request")
8+
message: str = Field(..., description="Message of the request", optional=True)

inference/core/env.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162
LICENSE_SERVER = os.getenv("LICENSE_SERVER", None)
163163

164164
# Log level, default is "INFO"
165-
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
165+
LOG_LEVEL = os.getenv("LOG_LEVEL", "WARNING")
166166

167167
# Maximum number of active models, default is 8
168168
MAX_ACTIVE_MODELS = int(os.getenv("MAX_ACTIVE_MODELS", 8))
@@ -204,6 +204,15 @@
204204
# Model ID, default is None
205205
MODEL_ID = os.getenv("MODEL_ID")
206206

207+
# Enable jupyter notebook server route, default is False
208+
NOTEBOOK_ENABLED = str2bool(os.getenv("NOTEBOOK_ENABLED", False))
209+
210+
# Jupyter notebook password, default is "roboflow"
211+
NOTEBOOK_PASSWORD = os.getenv("NOTEBOOK_PASSWORD", "roboflow")
212+
213+
# Jupyter notebook port, default is 9002
214+
NOTEBOOK_PORT = int(os.getenv("NOTEBOOK_PORT", 9002))
215+
207216
# Number of workers, default is 1
208217
NUM_WORKERS = int(os.getenv("NUM_WORKERS", 1))
209218

inference/core/interfaces/http/http_api.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import base64
22
import traceback
33
from functools import partial, wraps
4+
from time import sleep
45
from typing import Any, List, Optional, Union
56

67
import uvicorn
78
from fastapi import BackgroundTasks, Body, FastAPI, Path, Query, Request
89
from fastapi.middleware.cors import CORSMiddleware
9-
from fastapi.responses import JSONResponse, Response
10+
from fastapi.responses import JSONResponse, RedirectResponse, Response
1011
from fastapi.staticfiles import StaticFiles
1112
from fastapi_cprofile.profiler import CProfileMiddleware
1213

@@ -52,6 +53,7 @@
5253
ObjectDetectionInferenceResponse,
5354
StubResponse,
5455
)
56+
from inference.core.entities.responses.notebooks import NotebookStartResponse
5557
from inference.core.entities.responses.sam import (
5658
SamEmbeddingResponse,
5759
SamSegmentationResponse,
@@ -73,6 +75,9 @@
7375
LEGACY_ROUTE_ENABLED,
7476
METLO_KEY,
7577
METRICS_ENABLED,
78+
NOTEBOOK_ENABLED,
79+
NOTEBOOK_PASSWORD,
80+
NOTEBOOK_PORT,
7681
PROFILE,
7782
ROBOFLOW_SERVICE_SECRET,
7883
)
@@ -101,6 +106,7 @@
101106
from inference.core.interfaces.base import BaseInterface
102107
from inference.core.interfaces.http.orjson_utils import orjson_response
103108
from inference.core.managers.base import ModelManager
109+
from inference.core.utils.notebooks import start_notebook
104110

105111
if LAMBDA:
106112
from inference.core.usage import trackUsage
@@ -1200,6 +1206,45 @@ async def model_add(dataset_id: str, version_id: str, api_key: str = None):
12001206
}
12011207
)
12021208

1209+
if not LAMBDA:
1210+
1211+
@app.get(
1212+
"/notebook/start",
1213+
summary="Jupyter Lab Server Start",
1214+
description="Starts a jupyter lab server for running development code",
1215+
)
1216+
@with_route_exceptions
1217+
async def notebook_start(browserless: bool = False):
1218+
"""Starts a jupyter lab server for running development code.
1219+
1220+
Args:
1221+
inference_request (NotebookStartRequest): The request containing the necessary details for starting a jupyter lab server.
1222+
background_tasks: (BackgroundTasks) pool of fastapi background tasks
1223+
1224+
Returns:
1225+
NotebookStartResponse: The response containing the URL of the jupyter lab server.
1226+
"""
1227+
if NOTEBOOK_ENABLED:
1228+
start_notebook()
1229+
if browserless:
1230+
return {
1231+
"success": True,
1232+
"message": f"Jupyter Lab server started at http://localhost:{NOTEBOOK_PORT}?token={NOTEBOOK_PASSWORD}",
1233+
}
1234+
else:
1235+
sleep(2)
1236+
return RedirectResponse(
1237+
f"http://localhost:{NOTEBOOK_PORT}/lab/tree/quickstart.ipynb?token={NOTEBOOK_PASSWORD}"
1238+
)
1239+
else:
1240+
if browserless:
1241+
return {
1242+
"success": False,
1243+
"message": "Notebook server is not enabled. Enable notebooks via the NOTEBOOK_ENABLED environment variable.",
1244+
}
1245+
else:
1246+
return RedirectResponse(f"/notebook-instructions.html")
1247+
12031248
app.mount(
12041249
"/",
12051250
StaticFiles(directory="./inference/landing/out", html=True),

inference/core/interfaces/http/orjson_utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def orjson_response(
2929
response: Union[List[InferenceResponse], InferenceResponse]
3030
) -> ORJSONResponseBytes:
3131
if isinstance(response, list):
32-
content = [r.dict(by_alias=True) for r in response]
32+
content = [r.dict(by_alias=True, exclude_none=True) for r in response]
3333
else:
34-
content = response.dict(by_alias=True)
34+
content = response.dict(by_alias=True, exclude_none=True)
3535
return ORJSONResponseBytes(content=content)

inference/core/logger.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
import os
2+
import warnings
33

44
from rich.logging import RichHandler
55

@@ -8,3 +8,6 @@
88
logger = logging.getLogger("inference")
99
logger.setLevel(LOG_LEVEL)
1010
logger.addHandler(RichHandler())
11+
12+
if LOG_LEVEL == "ERROR" or LOG_LEVEL == "FATAL":
13+
warnings.filterwarnings("ignore", category=UserWarning, module="onnxruntime.*")

inference/core/utils/notebooks.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
import subprocess
3+
4+
import requests
5+
6+
from inference.core.env import NOTEBOOK_PASSWORD, NOTEBOOK_PORT
7+
8+
9+
def check_notebook_is_running():
10+
try:
11+
response = requests.get(f"http://localhost:{NOTEBOOK_PORT}/")
12+
return response.status_code == 200
13+
except:
14+
return False
15+
16+
17+
def start_notebook():
18+
if not check_notebook_is_running():
19+
os.makedirs("/notebooks", exist_ok=True)
20+
subprocess.Popen(
21+
f"jupyter-lab --allow-root --port={NOTEBOOK_PORT} --ip=0.0.0.0 --notebook-dir=/notebooks --NotebookApp.token='{NOTEBOOK_PASSWORD}' --NotebookApp.password='{NOTEBOOK_PASSWORD}'".split(
22+
" "
23+
)
24+
)

0 commit comments

Comments
 (0)