Spaces:
Runtime error
Runtime error
Initial checkin
Browse files- Dockerfile +5 -2
- README.md +29 -15
- app.py +39 -137
- penguins.csv +0 -345
- preload.py +4 -0
- requirements.txt +12 -4
- videoinput/__init__.py +8 -0
- videoinput/dist/index.css +12 -0
- videoinput/dist/index.js +518 -0
- videoinput/hf.py +16 -0
- videoinput/input.py +70 -0
- videoinput/package.json +29 -0
- videoinput/query.py +114 -0
- videoinput/srcts/audioSpinner.ts +240 -0
- videoinput/srcts/avSettingsMenu.ts +91 -0
- videoinput/srcts/index.ts +57 -0
- videoinput/srcts/videoClipper.ts +254 -0
- videoinput/tsconfig.json +14 -0
- videoinput/ui.py +114 -0
- videoinput/utils.py +80 -0
- videoinput/yarn.lock +225 -0
- www/Adelie.png +0 -0
- www/Chinstrap.png +0 -0
- www/Gentoo.png +0 -0
- www/penguins.png +0 -0
Dockerfile
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
-
FROM python:3.
|
2 |
|
3 |
WORKDIR /code
|
|
|
4 |
|
5 |
COPY ./requirements.txt /code/requirements.txt
|
6 |
|
@@ -8,6 +9,8 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
|
8 |
|
9 |
COPY . .
|
10 |
|
|
|
|
|
11 |
EXPOSE 7860
|
12 |
|
13 |
-
CMD ["shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
1 |
+
FROM python:3.10
|
2 |
|
3 |
WORKDIR /code
|
4 |
+
RUN apt install -y ffmpeg
|
5 |
|
6 |
COPY ./requirements.txt /code/requirements.txt
|
7 |
|
|
|
9 |
|
10 |
COPY . .
|
11 |
|
12 |
+
RUN python preload.py
|
13 |
+
|
14 |
EXPOSE 7860
|
15 |
|
16 |
+
CMD ["shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,20 +1,34 @@
|
|
1 |
-
|
2 |
-
title: Multimodal
|
3 |
-
emoji: 🌍
|
4 |
-
colorFrom: yellow
|
5 |
-
colorTo: indigo
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
license: mit
|
9 |
-
---
|
10 |
|
11 |
-
This is a
|
12 |
|
|
|
13 |
|
14 |
-
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
-
To learn more about this framework please see the [Documentation](https://shiny.rstudio.com/py/docs/overview.html).
|
|
|
1 |
+
# Video in, audio out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
+
This is a [Shiny for Python](https://shiny.posit.co/py/) app for easily interacting with GPT-4o via short webcam recordings.
|
4 |
|
5 |
+
## Installation
|
6 |
|
7 |
+
### ffmpeg
|
8 |
|
9 |
+
You will need the `ffmpeg` utility installed. Either use the [official installers](https://ffmpeg.org/download.html), or `brew install ffmpeg` (for macOS brew users) or `choco install ffmpeg` (for Windows chocolatey users).
|
10 |
+
|
11 |
+
### OpenAI API key
|
12 |
+
|
13 |
+
Create a file called `.env` in the root of the project and add the following line:
|
14 |
+
|
15 |
+
```
|
16 |
+
OPENAI_API_KEY=<your-api-key>
|
17 |
+
```
|
18 |
+
|
19 |
+
If you have an OpenAI account, you can generate an API key from [this page](https://platform.openai.com/api-keys).
|
20 |
+
|
21 |
+
### Python dependencies
|
22 |
+
|
23 |
+
```
|
24 |
+
pip install -r requirements.txt
|
25 |
+
```
|
26 |
+
|
27 |
+
## Usage
|
28 |
+
|
29 |
+
```
|
30 |
+
shiny run app.py --port 0 --launch-browser
|
31 |
+
```
|
32 |
+
|
33 |
+
This will launch a browser window with a video preview. Press Record, speak your prompt, and press Stop. The video will be processed and the response will be read aloud.
|
34 |
|
|
app.py
CHANGED
@@ -1,151 +1,53 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
import matplotlib.colors as mpl_colors
|
4 |
|
5 |
-
|
6 |
-
import
|
7 |
-
import shinyswatch
|
8 |
|
9 |
-
from
|
10 |
|
11 |
-
|
12 |
|
13 |
-
|
14 |
|
15 |
-
|
16 |
-
numeric_cols: List[str] = df.select_dtypes(include=["float64"]).columns.tolist()
|
17 |
-
species: List[str] = df["Species"].unique().tolist()
|
18 |
-
species.sort()
|
19 |
|
20 |
-
app_ui = ui.page_fillable(
|
21 |
-
shinyswatch.theme.minty(),
|
22 |
-
ui.layout_sidebar(
|
23 |
-
ui.sidebar(
|
24 |
-
# Artwork by @allison_horst
|
25 |
-
ui.input_selectize(
|
26 |
-
"xvar",
|
27 |
-
"X variable",
|
28 |
-
numeric_cols,
|
29 |
-
selected="Bill Length (mm)",
|
30 |
-
),
|
31 |
-
ui.input_selectize(
|
32 |
-
"yvar",
|
33 |
-
"Y variable",
|
34 |
-
numeric_cols,
|
35 |
-
selected="Bill Depth (mm)",
|
36 |
-
),
|
37 |
-
ui.input_checkbox_group(
|
38 |
-
"species", "Filter by species", species, selected=species
|
39 |
-
),
|
40 |
-
ui.hr(),
|
41 |
-
ui.input_switch("by_species", "Show species", value=True),
|
42 |
-
ui.input_switch("show_margins", "Show marginal plots", value=True),
|
43 |
-
),
|
44 |
-
ui.output_ui("value_boxes"),
|
45 |
-
ui.output_plot("scatter", fill=True),
|
46 |
-
ui.help_text(
|
47 |
-
"Artwork by ",
|
48 |
-
ui.a("@allison_horst", href="https://twitter.com/allison_horst"),
|
49 |
-
class_="text-end",
|
50 |
-
),
|
51 |
-
),
|
52 |
-
)
|
53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
-
|
56 |
-
@reactive.Calc
|
57 |
-
def filtered_df() -> pd.DataFrame:
|
58 |
-
"""Returns a Pandas data frame that includes only the desired rows"""
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
return df[df["Species"].isin(input.species())]
|
65 |
-
|
66 |
-
@output
|
67 |
-
@render.plot
|
68 |
-
def scatter():
|
69 |
-
"""Generates a plot for Shiny to display to the user"""
|
70 |
-
|
71 |
-
# The plotting function to use depends on whether margins are desired
|
72 |
-
plotfunc = sns.jointplot if input.show_margins() else sns.scatterplot
|
73 |
-
|
74 |
-
plotfunc(
|
75 |
-
data=filtered_df(),
|
76 |
-
x=input.xvar(),
|
77 |
-
y=input.yvar(),
|
78 |
-
palette=palette,
|
79 |
-
hue="Species" if input.by_species() else None,
|
80 |
-
hue_order=species,
|
81 |
-
legend=False,
|
82 |
-
)
|
83 |
-
|
84 |
-
@output
|
85 |
-
@render.ui
|
86 |
-
def value_boxes():
|
87 |
-
df = filtered_df()
|
88 |
-
|
89 |
-
def penguin_value_box(title: str, count: int, bgcol: str, showcase_img: str):
|
90 |
-
return ui.value_box(
|
91 |
-
title,
|
92 |
-
count,
|
93 |
-
{"class_": "pt-1 pb-0"},
|
94 |
-
showcase=ui.fill.as_fill_item(
|
95 |
-
ui.tags.img(
|
96 |
-
{"style": "object-fit:contain;"},
|
97 |
-
src=showcase_img,
|
98 |
-
)
|
99 |
-
),
|
100 |
-
theme_color=None,
|
101 |
-
style=f"background-color: {bgcol};",
|
102 |
)
|
|
|
103 |
|
104 |
-
if not input.by_species():
|
105 |
-
return penguin_value_box(
|
106 |
-
"Penguins",
|
107 |
-
len(df.index),
|
108 |
-
bg_palette["default"],
|
109 |
-
# Artwork by @allison_horst
|
110 |
-
showcase_img="penguins.png",
|
111 |
-
)
|
112 |
-
|
113 |
-
value_boxes = [
|
114 |
-
penguin_value_box(
|
115 |
-
name,
|
116 |
-
len(df[df["Species"] == name]),
|
117 |
-
bg_palette[name],
|
118 |
-
# Artwork by @allison_horst
|
119 |
-
showcase_img=f"{name}.png",
|
120 |
-
)
|
121 |
-
for name in species
|
122 |
-
# Only include boxes for _selected_ species
|
123 |
-
if name in input.species()
|
124 |
-
]
|
125 |
-
|
126 |
-
return ui.layout_column_wrap(*value_boxes, width = 1 / len(value_boxes))
|
127 |
-
|
128 |
-
|
129 |
-
# "darkorange", "purple", "cyan4"
|
130 |
-
colors = [[255, 140, 0], [160, 32, 240], [0, 139, 139]]
|
131 |
-
colors = [(r / 255.0, g / 255.0, b / 255.0) for r, g, b in colors]
|
132 |
-
|
133 |
-
palette: Dict[str, Tuple[float, float, float]] = {
|
134 |
-
"Adelie": colors[0],
|
135 |
-
"Chinstrap": colors[1],
|
136 |
-
"Gentoo": colors[2],
|
137 |
-
"default": sns.color_palette()[0], # type: ignore
|
138 |
-
}
|
139 |
|
140 |
-
|
141 |
-
|
142 |
-
for name, col in palette.items():
|
143 |
-
# Adjusted n_colors until `axe` accessibility did not complain about color contrast
|
144 |
-
bg_palette[name] = mpl_colors.to_hex(sns.light_palette(col, n_colors=7)[1]) # type: ignore
|
145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
|
147 |
-
|
148 |
-
|
149 |
-
server,
|
150 |
-
static_assets=str(www_dir),
|
151 |
-
)
|
|
|
1 |
+
import base64
|
2 |
+
import tempfile
|
|
|
3 |
|
4 |
+
from openai import AsyncOpenAI
|
5 |
+
from shiny.express import input, render, ui
|
|
|
6 |
|
7 |
+
from videoinput import audio_spinner, input_video_clip, process_video
|
8 |
|
9 |
+
client = AsyncOpenAI()
|
10 |
|
11 |
+
ui.page_opts(class_="py-5")
|
12 |
|
13 |
+
input_video_clip("clip")
|
|
|
|
|
|
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
+
@render.ui
|
17 |
+
async def show_clip():
|
18 |
+
clip = input.clip()
|
19 |
+
mime_type = clip["type"]
|
20 |
+
bytes = base64.b64decode(clip["bytes"])
|
21 |
+
# TODO: Use correct file extension based on mime type
|
22 |
+
with tempfile.TemporaryDirectory() as tempdir:
|
23 |
+
filename = tempfile.mktemp(dir=tempdir, suffix=get_video_extension(mime_type))
|
24 |
+
with open(filename, "wb") as file:
|
25 |
+
file.write(bytes)
|
26 |
+
file.close()
|
27 |
|
28 |
+
with ui.Progress() as p:
|
|
|
|
|
|
|
29 |
|
30 |
+
mp3_data_uri = await process_video(
|
31 |
+
client,
|
32 |
+
filename,
|
33 |
+
callback=lambda status: p.set(message=status),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
)
|
35 |
+
return audio_spinner(src=mp3_data_uri)
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
+
def get_video_extension(mime_type: str) -> str:
|
39 |
+
mime_type = mime_type.split(";")[0].strip()
|
|
|
|
|
|
|
40 |
|
41 |
+
# Dictionary to map MIME types to file extensions
|
42 |
+
mime_to_extension = {
|
43 |
+
"video/webm": ".webm",
|
44 |
+
"video/mp4": ".mp4",
|
45 |
+
"video/ogg": ".ogv",
|
46 |
+
"video/x-matroska": ".mkv",
|
47 |
+
"video/avi": ".avi",
|
48 |
+
"video/mpeg": ".mpeg",
|
49 |
+
"video/quicktime": ".mov",
|
50 |
+
}
|
51 |
|
52 |
+
# Return the appropriate file extension for the given MIME type
|
53 |
+
return mime_to_extension.get(mime_type, "")
|
|
|
|
|
|
penguins.csv
DELETED
@@ -1,345 +0,0 @@
|
|
1 |
-
Species,Island,Bill Length (mm),Bill Depth (mm),Flipper Length (mm),Body Mass (g),Sex,Year
|
2 |
-
Adelie,Torgersen,39.1,18.7,181,3750,male,2007
|
3 |
-
Adelie,Torgersen,39.5,17.4,186,3800,female,2007
|
4 |
-
Adelie,Torgersen,40.3,18,195,3250,female,2007
|
5 |
-
Adelie,Torgersen,NA,NA,NA,NA,NA,2007
|
6 |
-
Adelie,Torgersen,36.7,19.3,193,3450,female,2007
|
7 |
-
Adelie,Torgersen,39.3,20.6,190,3650,male,2007
|
8 |
-
Adelie,Torgersen,38.9,17.8,181,3625,female,2007
|
9 |
-
Adelie,Torgersen,39.2,19.6,195,4675,male,2007
|
10 |
-
Adelie,Torgersen,34.1,18.1,193,3475,NA,2007
|
11 |
-
Adelie,Torgersen,42,20.2,190,4250,NA,2007
|
12 |
-
Adelie,Torgersen,37.8,17.1,186,3300,NA,2007
|
13 |
-
Adelie,Torgersen,37.8,17.3,180,3700,NA,2007
|
14 |
-
Adelie,Torgersen,41.1,17.6,182,3200,female,2007
|
15 |
-
Adelie,Torgersen,38.6,21.2,191,3800,male,2007
|
16 |
-
Adelie,Torgersen,34.6,21.1,198,4400,male,2007
|
17 |
-
Adelie,Torgersen,36.6,17.8,185,3700,female,2007
|
18 |
-
Adelie,Torgersen,38.7,19,195,3450,female,2007
|
19 |
-
Adelie,Torgersen,42.5,20.7,197,4500,male,2007
|
20 |
-
Adelie,Torgersen,34.4,18.4,184,3325,female,2007
|
21 |
-
Adelie,Torgersen,46,21.5,194,4200,male,2007
|
22 |
-
Adelie,Biscoe,37.8,18.3,174,3400,female,2007
|
23 |
-
Adelie,Biscoe,37.7,18.7,180,3600,male,2007
|
24 |
-
Adelie,Biscoe,35.9,19.2,189,3800,female,2007
|
25 |
-
Adelie,Biscoe,38.2,18.1,185,3950,male,2007
|
26 |
-
Adelie,Biscoe,38.8,17.2,180,3800,male,2007
|
27 |
-
Adelie,Biscoe,35.3,18.9,187,3800,female,2007
|
28 |
-
Adelie,Biscoe,40.6,18.6,183,3550,male,2007
|
29 |
-
Adelie,Biscoe,40.5,17.9,187,3200,female,2007
|
30 |
-
Adelie,Biscoe,37.9,18.6,172,3150,female,2007
|
31 |
-
Adelie,Biscoe,40.5,18.9,180,3950,male,2007
|
32 |
-
Adelie,Dream,39.5,16.7,178,3250,female,2007
|
33 |
-
Adelie,Dream,37.2,18.1,178,3900,male,2007
|
34 |
-
Adelie,Dream,39.5,17.8,188,3300,female,2007
|
35 |
-
Adelie,Dream,40.9,18.9,184,3900,male,2007
|
36 |
-
Adelie,Dream,36.4,17,195,3325,female,2007
|
37 |
-
Adelie,Dream,39.2,21.1,196,4150,male,2007
|
38 |
-
Adelie,Dream,38.8,20,190,3950,male,2007
|
39 |
-
Adelie,Dream,42.2,18.5,180,3550,female,2007
|
40 |
-
Adelie,Dream,37.6,19.3,181,3300,female,2007
|
41 |
-
Adelie,Dream,39.8,19.1,184,4650,male,2007
|
42 |
-
Adelie,Dream,36.5,18,182,3150,female,2007
|
43 |
-
Adelie,Dream,40.8,18.4,195,3900,male,2007
|
44 |
-
Adelie,Dream,36,18.5,186,3100,female,2007
|
45 |
-
Adelie,Dream,44.1,19.7,196,4400,male,2007
|
46 |
-
Adelie,Dream,37,16.9,185,3000,female,2007
|
47 |
-
Adelie,Dream,39.6,18.8,190,4600,male,2007
|
48 |
-
Adelie,Dream,41.1,19,182,3425,male,2007
|
49 |
-
Adelie,Dream,37.5,18.9,179,2975,NA,2007
|
50 |
-
Adelie,Dream,36,17.9,190,3450,female,2007
|
51 |
-
Adelie,Dream,42.3,21.2,191,4150,male,2007
|
52 |
-
Adelie,Biscoe,39.6,17.7,186,3500,female,2008
|
53 |
-
Adelie,Biscoe,40.1,18.9,188,4300,male,2008
|
54 |
-
Adelie,Biscoe,35,17.9,190,3450,female,2008
|
55 |
-
Adelie,Biscoe,42,19.5,200,4050,male,2008
|
56 |
-
Adelie,Biscoe,34.5,18.1,187,2900,female,2008
|
57 |
-
Adelie,Biscoe,41.4,18.6,191,3700,male,2008
|
58 |
-
Adelie,Biscoe,39,17.5,186,3550,female,2008
|
59 |
-
Adelie,Biscoe,40.6,18.8,193,3800,male,2008
|
60 |
-
Adelie,Biscoe,36.5,16.6,181,2850,female,2008
|
61 |
-
Adelie,Biscoe,37.6,19.1,194,3750,male,2008
|
62 |
-
Adelie,Biscoe,35.7,16.9,185,3150,female,2008
|
63 |
-
Adelie,Biscoe,41.3,21.1,195,4400,male,2008
|
64 |
-
Adelie,Biscoe,37.6,17,185,3600,female,2008
|
65 |
-
Adelie,Biscoe,41.1,18.2,192,4050,male,2008
|
66 |
-
Adelie,Biscoe,36.4,17.1,184,2850,female,2008
|
67 |
-
Adelie,Biscoe,41.6,18,192,3950,male,2008
|
68 |
-
Adelie,Biscoe,35.5,16.2,195,3350,female,2008
|
69 |
-
Adelie,Biscoe,41.1,19.1,188,4100,male,2008
|
70 |
-
Adelie,Torgersen,35.9,16.6,190,3050,female,2008
|
71 |
-
Adelie,Torgersen,41.8,19.4,198,4450,male,2008
|
72 |
-
Adelie,Torgersen,33.5,19,190,3600,female,2008
|
73 |
-
Adelie,Torgersen,39.7,18.4,190,3900,male,2008
|
74 |
-
Adelie,Torgersen,39.6,17.2,196,3550,female,2008
|
75 |
-
Adelie,Torgersen,45.8,18.9,197,4150,male,2008
|
76 |
-
Adelie,Torgersen,35.5,17.5,190,3700,female,2008
|
77 |
-
Adelie,Torgersen,42.8,18.5,195,4250,male,2008
|
78 |
-
Adelie,Torgersen,40.9,16.8,191,3700,female,2008
|
79 |
-
Adelie,Torgersen,37.2,19.4,184,3900,male,2008
|
80 |
-
Adelie,Torgersen,36.2,16.1,187,3550,female,2008
|
81 |
-
Adelie,Torgersen,42.1,19.1,195,4000,male,2008
|
82 |
-
Adelie,Torgersen,34.6,17.2,189,3200,female,2008
|
83 |
-
Adelie,Torgersen,42.9,17.6,196,4700,male,2008
|
84 |
-
Adelie,Torgersen,36.7,18.8,187,3800,female,2008
|
85 |
-
Adelie,Torgersen,35.1,19.4,193,4200,male,2008
|
86 |
-
Adelie,Dream,37.3,17.8,191,3350,female,2008
|
87 |
-
Adelie,Dream,41.3,20.3,194,3550,male,2008
|
88 |
-
Adelie,Dream,36.3,19.5,190,3800,male,2008
|
89 |
-
Adelie,Dream,36.9,18.6,189,3500,female,2008
|
90 |
-
Adelie,Dream,38.3,19.2,189,3950,male,2008
|
91 |
-
Adelie,Dream,38.9,18.8,190,3600,female,2008
|
92 |
-
Adelie,Dream,35.7,18,202,3550,female,2008
|
93 |
-
Adelie,Dream,41.1,18.1,205,4300,male,2008
|
94 |
-
Adelie,Dream,34,17.1,185,3400,female,2008
|
95 |
-
Adelie,Dream,39.6,18.1,186,4450,male,2008
|
96 |
-
Adelie,Dream,36.2,17.3,187,3300,female,2008
|
97 |
-
Adelie,Dream,40.8,18.9,208,4300,male,2008
|
98 |
-
Adelie,Dream,38.1,18.6,190,3700,female,2008
|
99 |
-
Adelie,Dream,40.3,18.5,196,4350,male,2008
|
100 |
-
Adelie,Dream,33.1,16.1,178,2900,female,2008
|
101 |
-
Adelie,Dream,43.2,18.5,192,4100,male,2008
|
102 |
-
Adelie,Biscoe,35,17.9,192,3725,female,2009
|
103 |
-
Adelie,Biscoe,41,20,203,4725,male,2009
|
104 |
-
Adelie,Biscoe,37.7,16,183,3075,female,2009
|
105 |
-
Adelie,Biscoe,37.8,20,190,4250,male,2009
|
106 |
-
Adelie,Biscoe,37.9,18.6,193,2925,female,2009
|
107 |
-
Adelie,Biscoe,39.7,18.9,184,3550,male,2009
|
108 |
-
Adelie,Biscoe,38.6,17.2,199,3750,female,2009
|
109 |
-
Adelie,Biscoe,38.2,20,190,3900,male,2009
|
110 |
-
Adelie,Biscoe,38.1,17,181,3175,female,2009
|
111 |
-
Adelie,Biscoe,43.2,19,197,4775,male,2009
|
112 |
-
Adelie,Biscoe,38.1,16.5,198,3825,female,2009
|
113 |
-
Adelie,Biscoe,45.6,20.3,191,4600,male,2009
|
114 |
-
Adelie,Biscoe,39.7,17.7,193,3200,female,2009
|
115 |
-
Adelie,Biscoe,42.2,19.5,197,4275,male,2009
|
116 |
-
Adelie,Biscoe,39.6,20.7,191,3900,female,2009
|
117 |
-
Adelie,Biscoe,42.7,18.3,196,4075,male,2009
|
118 |
-
Adelie,Torgersen,38.6,17,188,2900,female,2009
|
119 |
-
Adelie,Torgersen,37.3,20.5,199,3775,male,2009
|
120 |
-
Adelie,Torgersen,35.7,17,189,3350,female,2009
|
121 |
-
Adelie,Torgersen,41.1,18.6,189,3325,male,2009
|
122 |
-
Adelie,Torgersen,36.2,17.2,187,3150,female,2009
|
123 |
-
Adelie,Torgersen,37.7,19.8,198,3500,male,2009
|
124 |
-
Adelie,Torgersen,40.2,17,176,3450,female,2009
|
125 |
-
Adelie,Torgersen,41.4,18.5,202,3875,male,2009
|
126 |
-
Adelie,Torgersen,35.2,15.9,186,3050,female,2009
|
127 |
-
Adelie,Torgersen,40.6,19,199,4000,male,2009
|
128 |
-
Adelie,Torgersen,38.8,17.6,191,3275,female,2009
|
129 |
-
Adelie,Torgersen,41.5,18.3,195,4300,male,2009
|
130 |
-
Adelie,Torgersen,39,17.1,191,3050,female,2009
|
131 |
-
Adelie,Torgersen,44.1,18,210,4000,male,2009
|
132 |
-
Adelie,Torgersen,38.5,17.9,190,3325,female,2009
|
133 |
-
Adelie,Torgersen,43.1,19.2,197,3500,male,2009
|
134 |
-
Adelie,Dream,36.8,18.5,193,3500,female,2009
|
135 |
-
Adelie,Dream,37.5,18.5,199,4475,male,2009
|
136 |
-
Adelie,Dream,38.1,17.6,187,3425,female,2009
|
137 |
-
Adelie,Dream,41.1,17.5,190,3900,male,2009
|
138 |
-
Adelie,Dream,35.6,17.5,191,3175,female,2009
|
139 |
-
Adelie,Dream,40.2,20.1,200,3975,male,2009
|
140 |
-
Adelie,Dream,37,16.5,185,3400,female,2009
|
141 |
-
Adelie,Dream,39.7,17.9,193,4250,male,2009
|
142 |
-
Adelie,Dream,40.2,17.1,193,3400,female,2009
|
143 |
-
Adelie,Dream,40.6,17.2,187,3475,male,2009
|
144 |
-
Adelie,Dream,32.1,15.5,188,3050,female,2009
|
145 |
-
Adelie,Dream,40.7,17,190,3725,male,2009
|
146 |
-
Adelie,Dream,37.3,16.8,192,3000,female,2009
|
147 |
-
Adelie,Dream,39,18.7,185,3650,male,2009
|
148 |
-
Adelie,Dream,39.2,18.6,190,4250,male,2009
|
149 |
-
Adelie,Dream,36.6,18.4,184,3475,female,2009
|
150 |
-
Adelie,Dream,36,17.8,195,3450,female,2009
|
151 |
-
Adelie,Dream,37.8,18.1,193,3750,male,2009
|
152 |
-
Adelie,Dream,36,17.1,187,3700,female,2009
|
153 |
-
Adelie,Dream,41.5,18.5,201,4000,male,2009
|
154 |
-
Gentoo,Biscoe,46.1,13.2,211,4500,female,2007
|
155 |
-
Gentoo,Biscoe,50,16.3,230,5700,male,2007
|
156 |
-
Gentoo,Biscoe,48.7,14.1,210,4450,female,2007
|
157 |
-
Gentoo,Biscoe,50,15.2,218,5700,male,2007
|
158 |
-
Gentoo,Biscoe,47.6,14.5,215,5400,male,2007
|
159 |
-
Gentoo,Biscoe,46.5,13.5,210,4550,female,2007
|
160 |
-
Gentoo,Biscoe,45.4,14.6,211,4800,female,2007
|
161 |
-
Gentoo,Biscoe,46.7,15.3,219,5200,male,2007
|
162 |
-
Gentoo,Biscoe,43.3,13.4,209,4400,female,2007
|
163 |
-
Gentoo,Biscoe,46.8,15.4,215,5150,male,2007
|
164 |
-
Gentoo,Biscoe,40.9,13.7,214,4650,female,2007
|
165 |
-
Gentoo,Biscoe,49,16.1,216,5550,male,2007
|
166 |
-
Gentoo,Biscoe,45.5,13.7,214,4650,female,2007
|
167 |
-
Gentoo,Biscoe,48.4,14.6,213,5850,male,2007
|
168 |
-
Gentoo,Biscoe,45.8,14.6,210,4200,female,2007
|
169 |
-
Gentoo,Biscoe,49.3,15.7,217,5850,male,2007
|
170 |
-
Gentoo,Biscoe,42,13.5,210,4150,female,2007
|
171 |
-
Gentoo,Biscoe,49.2,15.2,221,6300,male,2007
|
172 |
-
Gentoo,Biscoe,46.2,14.5,209,4800,female,2007
|
173 |
-
Gentoo,Biscoe,48.7,15.1,222,5350,male,2007
|
174 |
-
Gentoo,Biscoe,50.2,14.3,218,5700,male,2007
|
175 |
-
Gentoo,Biscoe,45.1,14.5,215,5000,female,2007
|
176 |
-
Gentoo,Biscoe,46.5,14.5,213,4400,female,2007
|
177 |
-
Gentoo,Biscoe,46.3,15.8,215,5050,male,2007
|
178 |
-
Gentoo,Biscoe,42.9,13.1,215,5000,female,2007
|
179 |
-
Gentoo,Biscoe,46.1,15.1,215,5100,male,2007
|
180 |
-
Gentoo,Biscoe,44.5,14.3,216,4100,NA,2007
|
181 |
-
Gentoo,Biscoe,47.8,15,215,5650,male,2007
|
182 |
-
Gentoo,Biscoe,48.2,14.3,210,4600,female,2007
|
183 |
-
Gentoo,Biscoe,50,15.3,220,5550,male,2007
|
184 |
-
Gentoo,Biscoe,47.3,15.3,222,5250,male,2007
|
185 |
-
Gentoo,Biscoe,42.8,14.2,209,4700,female,2007
|
186 |
-
Gentoo,Biscoe,45.1,14.5,207,5050,female,2007
|
187 |
-
Gentoo,Biscoe,59.6,17,230,6050,male,2007
|
188 |
-
Gentoo,Biscoe,49.1,14.8,220,5150,female,2008
|
189 |
-
Gentoo,Biscoe,48.4,16.3,220,5400,male,2008
|
190 |
-
Gentoo,Biscoe,42.6,13.7,213,4950,female,2008
|
191 |
-
Gentoo,Biscoe,44.4,17.3,219,5250,male,2008
|
192 |
-
Gentoo,Biscoe,44,13.6,208,4350,female,2008
|
193 |
-
Gentoo,Biscoe,48.7,15.7,208,5350,male,2008
|
194 |
-
Gentoo,Biscoe,42.7,13.7,208,3950,female,2008
|
195 |
-
Gentoo,Biscoe,49.6,16,225,5700,male,2008
|
196 |
-
Gentoo,Biscoe,45.3,13.7,210,4300,female,2008
|
197 |
-
Gentoo,Biscoe,49.6,15,216,4750,male,2008
|
198 |
-
Gentoo,Biscoe,50.5,15.9,222,5550,male,2008
|
199 |
-
Gentoo,Biscoe,43.6,13.9,217,4900,female,2008
|
200 |
-
Gentoo,Biscoe,45.5,13.9,210,4200,female,2008
|
201 |
-
Gentoo,Biscoe,50.5,15.9,225,5400,male,2008
|
202 |
-
Gentoo,Biscoe,44.9,13.3,213,5100,female,2008
|
203 |
-
Gentoo,Biscoe,45.2,15.8,215,5300,male,2008
|
204 |
-
Gentoo,Biscoe,46.6,14.2,210,4850,female,2008
|
205 |
-
Gentoo,Biscoe,48.5,14.1,220,5300,male,2008
|
206 |
-
Gentoo,Biscoe,45.1,14.4,210,4400,female,2008
|
207 |
-
Gentoo,Biscoe,50.1,15,225,5000,male,2008
|
208 |
-
Gentoo,Biscoe,46.5,14.4,217,4900,female,2008
|
209 |
-
Gentoo,Biscoe,45,15.4,220,5050,male,2008
|
210 |
-
Gentoo,Biscoe,43.8,13.9,208,4300,female,2008
|
211 |
-
Gentoo,Biscoe,45.5,15,220,5000,male,2008
|
212 |
-
Gentoo,Biscoe,43.2,14.5,208,4450,female,2008
|
213 |
-
Gentoo,Biscoe,50.4,15.3,224,5550,male,2008
|
214 |
-
Gentoo,Biscoe,45.3,13.8,208,4200,female,2008
|
215 |
-
Gentoo,Biscoe,46.2,14.9,221,5300,male,2008
|
216 |
-
Gentoo,Biscoe,45.7,13.9,214,4400,female,2008
|
217 |
-
Gentoo,Biscoe,54.3,15.7,231,5650,male,2008
|
218 |
-
Gentoo,Biscoe,45.8,14.2,219,4700,female,2008
|
219 |
-
Gentoo,Biscoe,49.8,16.8,230,5700,male,2008
|
220 |
-
Gentoo,Biscoe,46.2,14.4,214,4650,NA,2008
|
221 |
-
Gentoo,Biscoe,49.5,16.2,229,5800,male,2008
|
222 |
-
Gentoo,Biscoe,43.5,14.2,220,4700,female,2008
|
223 |
-
Gentoo,Biscoe,50.7,15,223,5550,male,2008
|
224 |
-
Gentoo,Biscoe,47.7,15,216,4750,female,2008
|
225 |
-
Gentoo,Biscoe,46.4,15.6,221,5000,male,2008
|
226 |
-
Gentoo,Biscoe,48.2,15.6,221,5100,male,2008
|
227 |
-
Gentoo,Biscoe,46.5,14.8,217,5200,female,2008
|
228 |
-
Gentoo,Biscoe,46.4,15,216,4700,female,2008
|
229 |
-
Gentoo,Biscoe,48.6,16,230,5800,male,2008
|
230 |
-
Gentoo,Biscoe,47.5,14.2,209,4600,female,2008
|
231 |
-
Gentoo,Biscoe,51.1,16.3,220,6000,male,2008
|
232 |
-
Gentoo,Biscoe,45.2,13.8,215,4750,female,2008
|
233 |
-
Gentoo,Biscoe,45.2,16.4,223,5950,male,2008
|
234 |
-
Gentoo,Biscoe,49.1,14.5,212,4625,female,2009
|
235 |
-
Gentoo,Biscoe,52.5,15.6,221,5450,male,2009
|
236 |
-
Gentoo,Biscoe,47.4,14.6,212,4725,female,2009
|
237 |
-
Gentoo,Biscoe,50,15.9,224,5350,male,2009
|
238 |
-
Gentoo,Biscoe,44.9,13.8,212,4750,female,2009
|
239 |
-
Gentoo,Biscoe,50.8,17.3,228,5600,male,2009
|
240 |
-
Gentoo,Biscoe,43.4,14.4,218,4600,female,2009
|
241 |
-
Gentoo,Biscoe,51.3,14.2,218,5300,male,2009
|
242 |
-
Gentoo,Biscoe,47.5,14,212,4875,female,2009
|
243 |
-
Gentoo,Biscoe,52.1,17,230,5550,male,2009
|
244 |
-
Gentoo,Biscoe,47.5,15,218,4950,female,2009
|
245 |
-
Gentoo,Biscoe,52.2,17.1,228,5400,male,2009
|
246 |
-
Gentoo,Biscoe,45.5,14.5,212,4750,female,2009
|
247 |
-
Gentoo,Biscoe,49.5,16.1,224,5650,male,2009
|
248 |
-
Gentoo,Biscoe,44.5,14.7,214,4850,female,2009
|
249 |
-
Gentoo,Biscoe,50.8,15.7,226,5200,male,2009
|
250 |
-
Gentoo,Biscoe,49.4,15.8,216,4925,male,2009
|
251 |
-
Gentoo,Biscoe,46.9,14.6,222,4875,female,2009
|
252 |
-
Gentoo,Biscoe,48.4,14.4,203,4625,female,2009
|
253 |
-
Gentoo,Biscoe,51.1,16.5,225,5250,male,2009
|
254 |
-
Gentoo,Biscoe,48.5,15,219,4850,female,2009
|
255 |
-
Gentoo,Biscoe,55.9,17,228,5600,male,2009
|
256 |
-
Gentoo,Biscoe,47.2,15.5,215,4975,female,2009
|
257 |
-
Gentoo,Biscoe,49.1,15,228,5500,male,2009
|
258 |
-
Gentoo,Biscoe,47.3,13.8,216,4725,NA,2009
|
259 |
-
Gentoo,Biscoe,46.8,16.1,215,5500,male,2009
|
260 |
-
Gentoo,Biscoe,41.7,14.7,210,4700,female,2009
|
261 |
-
Gentoo,Biscoe,53.4,15.8,219,5500,male,2009
|
262 |
-
Gentoo,Biscoe,43.3,14,208,4575,female,2009
|
263 |
-
Gentoo,Biscoe,48.1,15.1,209,5500,male,2009
|
264 |
-
Gentoo,Biscoe,50.5,15.2,216,5000,female,2009
|
265 |
-
Gentoo,Biscoe,49.8,15.9,229,5950,male,2009
|
266 |
-
Gentoo,Biscoe,43.5,15.2,213,4650,female,2009
|
267 |
-
Gentoo,Biscoe,51.5,16.3,230,5500,male,2009
|
268 |
-
Gentoo,Biscoe,46.2,14.1,217,4375,female,2009
|
269 |
-
Gentoo,Biscoe,55.1,16,230,5850,male,2009
|
270 |
-
Gentoo,Biscoe,44.5,15.7,217,4875,NA,2009
|
271 |
-
Gentoo,Biscoe,48.8,16.2,222,6000,male,2009
|
272 |
-
Gentoo,Biscoe,47.2,13.7,214,4925,female,2009
|
273 |
-
Gentoo,Biscoe,NA,NA,NA,NA,NA,2009
|
274 |
-
Gentoo,Biscoe,46.8,14.3,215,4850,female,2009
|
275 |
-
Gentoo,Biscoe,50.4,15.7,222,5750,male,2009
|
276 |
-
Gentoo,Biscoe,45.2,14.8,212,5200,female,2009
|
277 |
-
Gentoo,Biscoe,49.9,16.1,213,5400,male,2009
|
278 |
-
Chinstrap,Dream,46.5,17.9,192,3500,female,2007
|
279 |
-
Chinstrap,Dream,50,19.5,196,3900,male,2007
|
280 |
-
Chinstrap,Dream,51.3,19.2,193,3650,male,2007
|
281 |
-
Chinstrap,Dream,45.4,18.7,188,3525,female,2007
|
282 |
-
Chinstrap,Dream,52.7,19.8,197,3725,male,2007
|
283 |
-
Chinstrap,Dream,45.2,17.8,198,3950,female,2007
|
284 |
-
Chinstrap,Dream,46.1,18.2,178,3250,female,2007
|
285 |
-
Chinstrap,Dream,51.3,18.2,197,3750,male,2007
|
286 |
-
Chinstrap,Dream,46,18.9,195,4150,female,2007
|
287 |
-
Chinstrap,Dream,51.3,19.9,198,3700,male,2007
|
288 |
-
Chinstrap,Dream,46.6,17.8,193,3800,female,2007
|
289 |
-
Chinstrap,Dream,51.7,20.3,194,3775,male,2007
|
290 |
-
Chinstrap,Dream,47,17.3,185,3700,female,2007
|
291 |
-
Chinstrap,Dream,52,18.1,201,4050,male,2007
|
292 |
-
Chinstrap,Dream,45.9,17.1,190,3575,female,2007
|
293 |
-
Chinstrap,Dream,50.5,19.6,201,4050,male,2007
|
294 |
-
Chinstrap,Dream,50.3,20,197,3300,male,2007
|
295 |
-
Chinstrap,Dream,58,17.8,181,3700,female,2007
|
296 |
-
Chinstrap,Dream,46.4,18.6,190,3450,female,2007
|
297 |
-
Chinstrap,Dream,49.2,18.2,195,4400,male,2007
|
298 |
-
Chinstrap,Dream,42.4,17.3,181,3600,female,2007
|
299 |
-
Chinstrap,Dream,48.5,17.5,191,3400,male,2007
|
300 |
-
Chinstrap,Dream,43.2,16.6,187,2900,female,2007
|
301 |
-
Chinstrap,Dream,50.6,19.4,193,3800,male,2007
|
302 |
-
Chinstrap,Dream,46.7,17.9,195,3300,female,2007
|
303 |
-
Chinstrap,Dream,52,19,197,4150,male,2007
|
304 |
-
Chinstrap,Dream,50.5,18.4,200,3400,female,2008
|
305 |
-
Chinstrap,Dream,49.5,19,200,3800,male,2008
|
306 |
-
Chinstrap,Dream,46.4,17.8,191,3700,female,2008
|
307 |
-
Chinstrap,Dream,52.8,20,205,4550,male,2008
|
308 |
-
Chinstrap,Dream,40.9,16.6,187,3200,female,2008
|
309 |
-
Chinstrap,Dream,54.2,20.8,201,4300,male,2008
|
310 |
-
Chinstrap,Dream,42.5,16.7,187,3350,female,2008
|
311 |
-
Chinstrap,Dream,51,18.8,203,4100,male,2008
|
312 |
-
Chinstrap,Dream,49.7,18.6,195,3600,male,2008
|
313 |
-
Chinstrap,Dream,47.5,16.8,199,3900,female,2008
|
314 |
-
Chinstrap,Dream,47.6,18.3,195,3850,female,2008
|
315 |
-
Chinstrap,Dream,52,20.7,210,4800,male,2008
|
316 |
-
Chinstrap,Dream,46.9,16.6,192,2700,female,2008
|
317 |
-
Chinstrap,Dream,53.5,19.9,205,4500,male,2008
|
318 |
-
Chinstrap,Dream,49,19.5,210,3950,male,2008
|
319 |
-
Chinstrap,Dream,46.2,17.5,187,3650,female,2008
|
320 |
-
Chinstrap,Dream,50.9,19.1,196,3550,male,2008
|
321 |
-
Chinstrap,Dream,45.5,17,196,3500,female,2008
|
322 |
-
Chinstrap,Dream,50.9,17.9,196,3675,female,2009
|
323 |
-
Chinstrap,Dream,50.8,18.5,201,4450,male,2009
|
324 |
-
Chinstrap,Dream,50.1,17.9,190,3400,female,2009
|
325 |
-
Chinstrap,Dream,49,19.6,212,4300,male,2009
|
326 |
-
Chinstrap,Dream,51.5,18.7,187,3250,male,2009
|
327 |
-
Chinstrap,Dream,49.8,17.3,198,3675,female,2009
|
328 |
-
Chinstrap,Dream,48.1,16.4,199,3325,female,2009
|
329 |
-
Chinstrap,Dream,51.4,19,201,3950,male,2009
|
330 |
-
Chinstrap,Dream,45.7,17.3,193,3600,female,2009
|
331 |
-
Chinstrap,Dream,50.7,19.7,203,4050,male,2009
|
332 |
-
Chinstrap,Dream,42.5,17.3,187,3350,female,2009
|
333 |
-
Chinstrap,Dream,52.2,18.8,197,3450,male,2009
|
334 |
-
Chinstrap,Dream,45.2,16.6,191,3250,female,2009
|
335 |
-
Chinstrap,Dream,49.3,19.9,203,4050,male,2009
|
336 |
-
Chinstrap,Dream,50.2,18.8,202,3800,male,2009
|
337 |
-
Chinstrap,Dream,45.6,19.4,194,3525,female,2009
|
338 |
-
Chinstrap,Dream,51.9,19.5,206,3950,male,2009
|
339 |
-
Chinstrap,Dream,46.8,16.5,189,3650,female,2009
|
340 |
-
Chinstrap,Dream,45.7,17,195,3650,female,2009
|
341 |
-
Chinstrap,Dream,55.8,19.8,207,4000,male,2009
|
342 |
-
Chinstrap,Dream,43.5,18.1,202,3400,female,2009
|
343 |
-
Chinstrap,Dream,49.6,18.2,193,3775,male,2009
|
344 |
-
Chinstrap,Dream,50.8,19,210,4100,male,2009
|
345 |
-
Chinstrap,Dream,50.2,18.7,198,3775,female,2009
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
preload.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import AutoProcessor, AutoModelForVision2Seq
|
2 |
+
|
3 |
+
processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b")
|
4 |
+
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b")
|
requirements.txt
CHANGED
@@ -1,4 +1,12 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
--extra-index-url https://pypi.ngc.nvidia.com
|
2 |
+
colorama
|
3 |
+
faicons
|
4 |
+
ffmpeg-python
|
5 |
+
openai
|
6 |
+
python-dotenv
|
7 |
+
shiny
|
8 |
+
nvidia-cuda-runtime-cu12
|
9 |
+
nvidia-cudnn
|
10 |
+
requests
|
11 |
+
torch
|
12 |
+
transformers
|
videoinput/__init__.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .ui import input_video_clip, audio_spinner
|
2 |
+
from .query import process_video
|
3 |
+
|
4 |
+
__all__ = (
|
5 |
+
"input_video_clip",
|
6 |
+
"audio_spinner",
|
7 |
+
"process_video",
|
8 |
+
)
|
videoinput/dist/index.css
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
av-settings-menu {
|
2 |
+
display: block;
|
3 |
+
width: min-content;
|
4 |
+
}
|
5 |
+
|
6 |
+
/* The normal treatment of .dropdown-item.active is a little too much */
|
7 |
+
av-settings-menu .dropdown-item.active,
|
8 |
+
av-settings-menu .dropdown-menu > li > a.active {
|
9 |
+
color: inherit;
|
10 |
+
background-color: inherit;
|
11 |
+
font-weight: bold;
|
12 |
+
}
|
videoinput/dist/index.js
ADDED
@@ -0,0 +1,518 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use strict";
|
2 |
+
|
3 |
+
// srcts/videoClipper.ts
|
4 |
+
var VideoClipperElement = class extends HTMLElement {
|
5 |
+
constructor() {
|
6 |
+
super();
|
7 |
+
this.chunks = [];
|
8 |
+
this.attachShadow({ mode: "open" });
|
9 |
+
this.shadowRoot.innerHTML = `
|
10 |
+
<style>
|
11 |
+
:host {
|
12 |
+
display: grid;
|
13 |
+
grid-template-rows: 1fr;
|
14 |
+
grid-template-columns: 1fr;
|
15 |
+
width: 100%;
|
16 |
+
height: min-content;
|
17 |
+
}
|
18 |
+
video {
|
19 |
+
grid-column: 1 / 2;
|
20 |
+
grid-row: 1 / 2;
|
21 |
+
width: 100%;
|
22 |
+
object-fit: cover;
|
23 |
+
background-color: var(--video-clip-bg, black);
|
24 |
+
aspect-ratio: 16 / 9;
|
25 |
+
border-radius: var(--video-clip-border-radius, var(--bs-border-radius-lg));
|
26 |
+
}
|
27 |
+
video.mirrored {
|
28 |
+
transform: scaleX(-1);
|
29 |
+
}
|
30 |
+
.panel-settings {
|
31 |
+
grid-column: 1 / 2;
|
32 |
+
grid-row: 1 / 2;
|
33 |
+
justify-self: end;
|
34 |
+
margin: 0.5em;
|
35 |
+
}
|
36 |
+
.panel-buttons {
|
37 |
+
grid-column: 1 / 2;
|
38 |
+
grid-row: 1 / 2;
|
39 |
+
justify-self: end;
|
40 |
+
align-self: end;
|
41 |
+
margin: 0.5em;
|
42 |
+
}
|
43 |
+
</style>
|
44 |
+
<video part="video" muted></video>
|
45 |
+
<div class="panel-settings">
|
46 |
+
<slot name="settings"></slot>
|
47 |
+
</div>
|
48 |
+
<div class="panel-buttons">
|
49 |
+
<slot name="recording-controls"></slot>
|
50 |
+
</div>
|
51 |
+
`;
|
52 |
+
this.video = this.shadowRoot.querySelector("video");
|
53 |
+
}
|
54 |
+
connectedCallback() {
|
55 |
+
(async () => {
|
56 |
+
const slotSettings = this.shadowRoot.querySelector(
|
57 |
+
"slot[name=settings]"
|
58 |
+
);
|
59 |
+
slotSettings.addEventListener("slotchange", async () => {
|
60 |
+
this.avSettingsMenu = slotSettings.assignedElements()[0];
|
61 |
+
await this.#initializeMediaInput();
|
62 |
+
if (this.buttonRecord) {
|
63 |
+
this.#setEnabledButton(this.buttonRecord);
|
64 |
+
}
|
65 |
+
});
|
66 |
+
const slotControls = this.shadowRoot.querySelector(
|
67 |
+
"slot[name=recording-controls]"
|
68 |
+
);
|
69 |
+
slotControls.addEventListener("slotchange", () => {
|
70 |
+
const findButton = (selector) => {
|
71 |
+
for (const el of slotControls.assignedElements()) {
|
72 |
+
if (el.matches(selector)) {
|
73 |
+
return el;
|
74 |
+
}
|
75 |
+
const sub = el.querySelector(selector);
|
76 |
+
if (sub) {
|
77 |
+
return sub;
|
78 |
+
}
|
79 |
+
}
|
80 |
+
return null;
|
81 |
+
};
|
82 |
+
this.buttonRecord = findButton(".record-button");
|
83 |
+
this.buttonStop = findButton(".stop-button");
|
84 |
+
this.#setEnabledButton();
|
85 |
+
this.buttonRecord.addEventListener("click", () => {
|
86 |
+
this.#setEnabledButton(this.buttonStop);
|
87 |
+
this._beginRecord();
|
88 |
+
});
|
89 |
+
this.buttonStop.addEventListener("click", () => {
|
90 |
+
this._endRecord();
|
91 |
+
this.#setEnabledButton(this.buttonRecord);
|
92 |
+
});
|
93 |
+
});
|
94 |
+
})().catch((err) => {
|
95 |
+
console.error(err);
|
96 |
+
});
|
97 |
+
}
|
98 |
+
disconnectedCallback() {
|
99 |
+
}
|
100 |
+
#setEnabledButton(btn) {
|
101 |
+
this.buttonRecord.style.display = btn === this.buttonRecord ? "inline-block" : "none";
|
102 |
+
this.buttonStop.style.display = btn === this.buttonStop ? "inline-block" : "none";
|
103 |
+
}
|
104 |
+
async setMediaDevices(cameraId, micId) {
|
105 |
+
if (this.cameraStream) {
|
106 |
+
this.cameraStream.getTracks().forEach((track) => track.stop());
|
107 |
+
}
|
108 |
+
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
109 |
+
video: {
|
110 |
+
deviceId: cameraId || void 0,
|
111 |
+
facingMode: "user",
|
112 |
+
aspectRatio: 16 / 9
|
113 |
+
},
|
114 |
+
audio: {
|
115 |
+
deviceId: micId || void 0
|
116 |
+
}
|
117 |
+
});
|
118 |
+
const isSelfieCam = true;
|
119 |
+
this.video.classList.toggle("mirrored", isSelfieCam);
|
120 |
+
const aspectRatio = this.cameraStream.getVideoTracks()[0].getSettings().aspectRatio;
|
121 |
+
if (aspectRatio) {
|
122 |
+
this.video.style.aspectRatio = `${aspectRatio}`;
|
123 |
+
} else {
|
124 |
+
this.video.style.aspectRatio = "";
|
125 |
+
}
|
126 |
+
this.video.srcObject = this.cameraStream;
|
127 |
+
this.video.play();
|
128 |
+
return {
|
129 |
+
cameraId: this.cameraStream.getVideoTracks()[0].getSettings().deviceId,
|
130 |
+
micId: this.cameraStream.getAudioTracks()[0].getSettings().deviceId
|
131 |
+
};
|
132 |
+
}
|
133 |
+
async #initializeMediaInput() {
|
134 |
+
const savedCamera = window.localStorage.getItem("multimodal-camera");
|
135 |
+
const savedMic = window.localStorage.getItem("multimodal-mic");
|
136 |
+
const { cameraId, micId } = await this.setMediaDevices(
|
137 |
+
savedCamera,
|
138 |
+
savedMic
|
139 |
+
);
|
140 |
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
141 |
+
this.avSettingsMenu.setCameras(
|
142 |
+
devices.filter((dev) => dev.kind === "videoinput")
|
143 |
+
);
|
144 |
+
this.avSettingsMenu.setMics(
|
145 |
+
devices.filter((dev) => dev.kind === "audioinput")
|
146 |
+
);
|
147 |
+
this.avSettingsMenu.cameraId = cameraId;
|
148 |
+
this.avSettingsMenu.micId = micId;
|
149 |
+
const handleDeviceChange = async (deviceType, deviceId) => {
|
150 |
+
if (!deviceId) return;
|
151 |
+
window.localStorage.setItem(`multimodal-${deviceType}`, deviceId);
|
152 |
+
await this.setMediaDevices(
|
153 |
+
this.avSettingsMenu.cameraId,
|
154 |
+
this.avSettingsMenu.micId
|
155 |
+
);
|
156 |
+
};
|
157 |
+
this.avSettingsMenu.addEventListener("camera-change", (e) => {
|
158 |
+
handleDeviceChange("camera", this.avSettingsMenu.cameraId);
|
159 |
+
});
|
160 |
+
this.avSettingsMenu.addEventListener("mic-change", (e) => {
|
161 |
+
handleDeviceChange("mic", this.avSettingsMenu.micId);
|
162 |
+
});
|
163 |
+
}
|
164 |
+
_beginRecord() {
|
165 |
+
this.recorder = new MediaRecorder(this.cameraStream, {});
|
166 |
+
this.recorder.addEventListener("error", (e) => {
|
167 |
+
console.error("MediaRecorder error:", e.error);
|
168 |
+
});
|
169 |
+
this.recorder.addEventListener("dataavailable", (e) => {
|
170 |
+
this.chunks.push(e.data);
|
171 |
+
});
|
172 |
+
this.recorder.addEventListener("start", () => {
|
173 |
+
});
|
174 |
+
this.recorder.start();
|
175 |
+
}
|
176 |
+
_endRecord(emit = true) {
|
177 |
+
this.recorder.stop();
|
178 |
+
if (!emit) {
|
179 |
+
this.chunks = [];
|
180 |
+
} else {
|
181 |
+
setTimeout(() => {
|
182 |
+
const blob = new Blob(this.chunks, { type: this.chunks[0].type });
|
183 |
+
const event = new BlobEvent("data", {
|
184 |
+
data: blob
|
185 |
+
});
|
186 |
+
try {
|
187 |
+
this.dispatchEvent(event);
|
188 |
+
} finally {
|
189 |
+
this.chunks = [];
|
190 |
+
}
|
191 |
+
}, 0);
|
192 |
+
}
|
193 |
+
}
|
194 |
+
};
|
195 |
+
customElements.define("video-clipper", VideoClipperElement);
|
196 |
+
|
197 |
+
// srcts/avSettingsMenu.ts
|
198 |
+
var DeviceChangeEvent = class extends CustomEvent {
|
199 |
+
constructor(type, detail) {
|
200 |
+
super(type, { detail });
|
201 |
+
}
|
202 |
+
};
|
203 |
+
var AVSettingsMenuElement = class extends HTMLElement {
|
204 |
+
constructor() {
|
205 |
+
super();
|
206 |
+
this.addEventListener("click", (e) => {
|
207 |
+
if (e.target instanceof HTMLAnchorElement) {
|
208 |
+
const a = e.target;
|
209 |
+
if (a.classList.contains("camera-device-item")) {
|
210 |
+
this.cameraId = a.dataset.deviceId;
|
211 |
+
} else if (a.classList.contains("mic-device-item")) {
|
212 |
+
this.micId = a.dataset.deviceId;
|
213 |
+
}
|
214 |
+
}
|
215 |
+
});
|
216 |
+
}
|
217 |
+
#setDevices(deviceType, devices) {
|
218 |
+
const deviceEls = devices.map(
|
219 |
+
(dev) => this.#createDeviceElement(dev, `${deviceType}-device-item`)
|
220 |
+
);
|
221 |
+
const header = this.querySelector(`.${deviceType}-header`);
|
222 |
+
header.after(...deviceEls);
|
223 |
+
}
|
224 |
+
setCameras(cameras) {
|
225 |
+
this.#setDevices("camera", cameras);
|
226 |
+
}
|
227 |
+
setMics(mics) {
|
228 |
+
this.#setDevices("mic", mics);
|
229 |
+
}
|
230 |
+
get cameraId() {
|
231 |
+
return this.#getSelectedDevice("camera");
|
232 |
+
}
|
233 |
+
set cameraId(id) {
|
234 |
+
this.#setSelectedDevice("camera", id);
|
235 |
+
}
|
236 |
+
get micId() {
|
237 |
+
return this.#getSelectedDevice("mic");
|
238 |
+
}
|
239 |
+
set micId(id) {
|
240 |
+
this.#setSelectedDevice("mic", id);
|
241 |
+
}
|
242 |
+
#createDeviceElement(dev, className) {
|
243 |
+
const li = this.ownerDocument.createElement("li");
|
244 |
+
const a = li.appendChild(this.ownerDocument.createElement("a"));
|
245 |
+
a.onclick = (e) => e.preventDefault();
|
246 |
+
a.href = "#";
|
247 |
+
a.textContent = dev.label;
|
248 |
+
a.dataset.deviceId = dev.deviceId;
|
249 |
+
a.className = className;
|
250 |
+
return li;
|
251 |
+
}
|
252 |
+
#getSelectedDevice(device) {
|
253 |
+
return this.querySelector(
|
254 |
+
`a.${device}-device-item.active`
|
255 |
+
)?.dataset.deviceId ?? null;
|
256 |
+
}
|
257 |
+
#setSelectedDevice(device, id) {
|
258 |
+
this.querySelectorAll(`a.${device}-device-item.active`).forEach(
|
259 |
+
(a) => a.classList.remove("active")
|
260 |
+
);
|
261 |
+
if (id) {
|
262 |
+
this.querySelector(
|
263 |
+
`a.${device}-device-item[data-device-id="${id}"]`
|
264 |
+
).classList.add("active");
|
265 |
+
}
|
266 |
+
this.dispatchEvent(
|
267 |
+
new DeviceChangeEvent(`${device}-change`, {
|
268 |
+
deviceId: id
|
269 |
+
})
|
270 |
+
);
|
271 |
+
}
|
272 |
+
};
|
273 |
+
customElements.define("av-settings-menu", AVSettingsMenuElement);
|
274 |
+
|
275 |
+
// srcts/audioSpinner.ts
|
276 |
+
var AudioSpinnerElement = class extends HTMLElement {
|
277 |
+
#audio;
|
278 |
+
#canvas;
|
279 |
+
#ctx2d;
|
280 |
+
#analyzer;
|
281 |
+
#dataArray;
|
282 |
+
#smoother;
|
283 |
+
constructor() {
|
284 |
+
super();
|
285 |
+
this.attachShadow({ mode: "open" });
|
286 |
+
this.shadowRoot.innerHTML = `
|
287 |
+
<style>
|
288 |
+
:host {
|
289 |
+
display: block;
|
290 |
+
position: relative;
|
291 |
+
}
|
292 |
+
::slotted(canvas) {
|
293 |
+
position: absolute;
|
294 |
+
top: 0;
|
295 |
+
left: 0;
|
296 |
+
}
|
297 |
+
::slotted(audio) {
|
298 |
+
display: none;
|
299 |
+
}
|
300 |
+
</style>
|
301 |
+
<slot name="audio"></slot>
|
302 |
+
<slot name="canvas"></slot>
|
303 |
+
`;
|
304 |
+
}
|
305 |
+
connectedCallback() {
|
306 |
+
const audioSlot = this.shadowRoot.querySelector(
|
307 |
+
"slot[name=audio]"
|
308 |
+
);
|
309 |
+
this.#audio = this.ownerDocument.createElement("audio");
|
310 |
+
this.#audio.autoplay = true;
|
311 |
+
this.#audio.controls = false;
|
312 |
+
this.#audio.src = this.getAttribute("src");
|
313 |
+
this.#audio.slot = "audio";
|
314 |
+
audioSlot.assign(this.#audio);
|
315 |
+
this.#audio.addEventListener("play", () => {
|
316 |
+
this.#draw();
|
317 |
+
});
|
318 |
+
this.#audio.onpause = () => {
|
319 |
+
this.style.transition = "opacity 0.5s 1s";
|
320 |
+
this.classList.add("fade");
|
321 |
+
this.addEventListener("transitionend", () => {
|
322 |
+
this.remove();
|
323 |
+
});
|
324 |
+
};
|
325 |
+
const canvasSlot = this.shadowRoot.querySelector(
|
326 |
+
"slot[name=canvas]"
|
327 |
+
);
|
328 |
+
this.#canvas = this.ownerDocument.createElement("canvas");
|
329 |
+
this.#canvas.slot = "canvas";
|
330 |
+
this.#canvas.width = this.clientWidth * window.devicePixelRatio;
|
331 |
+
this.#canvas.height = this.clientHeight * window.devicePixelRatio;
|
332 |
+
this.#canvas.style.width = this.clientWidth + "px";
|
333 |
+
this.#canvas.style.height = this.clientHeight + "px";
|
334 |
+
this.appendChild(this.#canvas);
|
335 |
+
canvasSlot.assign(this.#canvas);
|
336 |
+
this.#ctx2d = this.#canvas.getContext("2d");
|
337 |
+
this.#ctx2d.scale(window.devicePixelRatio, window.devicePixelRatio);
|
338 |
+
new ResizeObserver(() => {
|
339 |
+
this.#canvas.width = this.clientWidth;
|
340 |
+
this.#canvas.height = this.clientHeight;
|
341 |
+
}).observe(this);
|
342 |
+
const audioCtx = new AudioContext();
|
343 |
+
const source = audioCtx.createMediaElementSource(this.#audio);
|
344 |
+
this.#analyzer = new AnalyserNode(audioCtx, {
|
345 |
+
fftSize: 2048
|
346 |
+
});
|
347 |
+
this.#dataArray = new Float32Array(this.#analyzer.frequencyBinCount);
|
348 |
+
source.connect(this.#analyzer);
|
349 |
+
this.#analyzer.connect(audioCtx.destination);
|
350 |
+
const dataArray2 = new Float32Array(this.#analyzer.frequencyBinCount);
|
351 |
+
this.#smoother = new Smoother(5, (samples) => {
|
352 |
+
for (let i = 0; i < dataArray2.length; i++) {
|
353 |
+
dataArray2[i] = 0;
|
354 |
+
for (let j = 0; j < samples.length; j++) {
|
355 |
+
dataArray2[i] += samples[j][i];
|
356 |
+
}
|
357 |
+
dataArray2[i] /= samples.length;
|
358 |
+
}
|
359 |
+
return dataArray2;
|
360 |
+
});
|
361 |
+
this.#draw();
|
362 |
+
}
|
363 |
+
#draw() {
|
364 |
+
if (!this.isConnected) {
|
365 |
+
return;
|
366 |
+
}
|
367 |
+
requestAnimationFrame(() => this.#draw());
|
368 |
+
const width = this.#canvas.width;
|
369 |
+
const height = this.#canvas.height;
|
370 |
+
this.#ctx2d.clearRect(0, 0, width, height);
|
371 |
+
this.#analyzer.getFloatTimeDomainData(this.#dataArray);
|
372 |
+
const smoothed = this.#smoother.add(new Float32Array(this.#dataArray));
|
373 |
+
const {
|
374 |
+
spinVelocity,
|
375 |
+
gap,
|
376 |
+
thickness,
|
377 |
+
minRadius,
|
378 |
+
radiusFactor,
|
379 |
+
steps,
|
380 |
+
blades
|
381 |
+
} = this.#getSettings(width, height);
|
382 |
+
const avg = smoothed.reduce((a, b) => a + Math.abs(b), 0) / smoothed.length * 4;
|
383 |
+
const radius = minRadius + avg * (height - minRadius) / radiusFactor;
|
384 |
+
for (let step = 0; step < steps; step++) {
|
385 |
+
const this_radius = radius - step * (radius / (steps + 1));
|
386 |
+
if (step === steps - 1) {
|
387 |
+
this.#drawPie(width, height, 0, Math.PI * 2, this_radius, thickness);
|
388 |
+
} else {
|
389 |
+
const seconds = (/* @__PURE__ */ new Date()).getTime() / 1e3;
|
390 |
+
const startAngle = seconds * spinVelocity % (Math.PI * 2);
|
391 |
+
for (let blade = 0; blade < blades; blade++) {
|
392 |
+
const angleOffset = Math.PI * 2 / blades * blade;
|
393 |
+
const sweep = Math.PI * 2 / blades - gap;
|
394 |
+
this.#drawPie(
|
395 |
+
width,
|
396 |
+
height,
|
397 |
+
startAngle + angleOffset,
|
398 |
+
sweep,
|
399 |
+
this_radius,
|
400 |
+
thickness
|
401 |
+
);
|
402 |
+
}
|
403 |
+
}
|
404 |
+
}
|
405 |
+
}
|
406 |
+
#drawPie(width, height, startAngle, sweep, radius, thickness) {
|
407 |
+
this.#ctx2d.beginPath();
|
408 |
+
this.#ctx2d.fillStyle = this.#canvas.computedStyleMap().get("color")?.toString();
|
409 |
+
if (!thickness) {
|
410 |
+
this.#ctx2d.moveTo(width / 2, height / 2);
|
411 |
+
}
|
412 |
+
this.#ctx2d.arc(
|
413 |
+
width / 2,
|
414 |
+
height / 2,
|
415 |
+
radius,
|
416 |
+
startAngle,
|
417 |
+
startAngle + sweep
|
418 |
+
);
|
419 |
+
if (!thickness) {
|
420 |
+
this.#ctx2d.lineTo(width / 2, height / 2);
|
421 |
+
} else {
|
422 |
+
this.#ctx2d.arc(
|
423 |
+
width / 2,
|
424 |
+
height / 2,
|
425 |
+
radius - thickness,
|
426 |
+
startAngle + sweep,
|
427 |
+
startAngle,
|
428 |
+
true
|
429 |
+
);
|
430 |
+
}
|
431 |
+
this.#ctx2d.fill();
|
432 |
+
}
|
433 |
+
#getSettings(width, height) {
|
434 |
+
const settings = {
|
435 |
+
spinVelocity: 5,
|
436 |
+
gap: Math.PI / 5,
|
437 |
+
thickness: 2.5,
|
438 |
+
minRadius: Math.min(width, height) / 6,
|
439 |
+
radiusFactor: 1.8,
|
440 |
+
steps: 3,
|
441 |
+
blades: 3
|
442 |
+
};
|
443 |
+
for (const key in settings) {
|
444 |
+
const value = tryParseFloat(this.dataset[key]);
|
445 |
+
if (typeof value !== "undefined") {
|
446 |
+
Object.assign(settings, { [key]: value });
|
447 |
+
}
|
448 |
+
}
|
449 |
+
return settings;
|
450 |
+
}
|
451 |
+
};
|
452 |
+
window.customElements.define("audio-spinner", AudioSpinnerElement);
|
453 |
+
var Smoother = class {
|
454 |
+
#samples = [];
|
455 |
+
#smooth;
|
456 |
+
#size;
|
457 |
+
#pos;
|
458 |
+
constructor(size, smooth) {
|
459 |
+
this.#size = size;
|
460 |
+
this.#pos = 0;
|
461 |
+
this.#smooth = smooth;
|
462 |
+
}
|
463 |
+
add(sample) {
|
464 |
+
this.#samples[this.#pos] = sample;
|
465 |
+
this.#pos = (this.#pos + 1) % this.#size;
|
466 |
+
return this.smoothed();
|
467 |
+
}
|
468 |
+
smoothed() {
|
469 |
+
return this.#smooth(this.#samples);
|
470 |
+
}
|
471 |
+
};
|
472 |
+
function tryParseFloat(str) {
|
473 |
+
if (typeof str === "undefined") {
|
474 |
+
return void 0;
|
475 |
+
}
|
476 |
+
const parsed = parseFloat(str);
|
477 |
+
return isNaN(parsed) ? void 0 : parsed;
|
478 |
+
}
|
479 |
+
|
480 |
+
// srcts/index.ts
|
481 |
+
var VideoClipperBinding = class extends Shiny.InputBinding {
|
482 |
+
#lastKnownValue = /* @__PURE__ */ new WeakMap();
|
483 |
+
#handlers = /* @__PURE__ */ new WeakMap();
|
484 |
+
find(scope) {
|
485 |
+
return $(scope).find("video-clipper");
|
486 |
+
}
|
487 |
+
getValue(el) {
|
488 |
+
return this.#lastKnownValue.get(el);
|
489 |
+
}
|
490 |
+
subscribe(el, callback) {
|
491 |
+
const handler = async (ev) => {
|
492 |
+
const blob = ev.data;
|
493 |
+
this.#lastKnownValue.set(el, {
|
494 |
+
type: blob.type,
|
495 |
+
bytes: await base64(blob)
|
496 |
+
});
|
497 |
+
callback(true);
|
498 |
+
};
|
499 |
+
el.addEventListener("data", handler);
|
500 |
+
this.#handlers.set(el, handler);
|
501 |
+
}
|
502 |
+
unsubscribe(el) {
|
503 |
+
const handler = this.#handlers.get(el);
|
504 |
+
el.removeEventListener("data", handler);
|
505 |
+
this.#handlers.delete(el);
|
506 |
+
}
|
507 |
+
};
|
508 |
+
window.Shiny.inputBindings.register(new VideoClipperBinding(), "video-clipper");
|
509 |
+
async function base64(blob) {
|
510 |
+
const buf = await blob.arrayBuffer();
|
511 |
+
const results = [];
|
512 |
+
const CHUNKSIZE = 1024;
|
513 |
+
for (let i = 0; i < buf.byteLength; i += CHUNKSIZE) {
|
514 |
+
const chunk = buf.slice(i, i + CHUNKSIZE);
|
515 |
+
results.push(String.fromCharCode(...new Uint8Array(chunk)));
|
516 |
+
}
|
517 |
+
return btoa(results.join(""));
|
518 |
+
}
|
videoinput/hf.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import torch
|
3 |
+
from PIL import Image
|
4 |
+
from io import BytesIO
|
5 |
+
|
6 |
+
from transformers import AutoProcessor, AutoModelForVision2Seq
|
7 |
+
from transformers.image_utils import load_image
|
8 |
+
|
9 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
10 |
+
|
11 |
+
image1 = load_image(
|
12 |
+
"https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_960_720.jpg"
|
13 |
+
)
|
14 |
+
|
15 |
+
processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b")
|
16 |
+
model = AutoModelForVision2Seq.from_pretrained("HuggingFaceM4/idefics2-8b").to(DEVICE)
|
videoinput/input.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import tempfile
|
3 |
+
from pathlib import Path, PurePath
|
4 |
+
|
5 |
+
import ffmpeg
|
6 |
+
|
7 |
+
|
8 |
+
class DecodedInput:
|
9 |
+
audio: PurePath
|
10 |
+
images: tuple[PurePath, ...]
|
11 |
+
|
12 |
+
def __init__(
|
13 |
+
self,
|
14 |
+
audio: PurePath,
|
15 |
+
images: tuple[PurePath, ...],
|
16 |
+
tmpdir: tempfile.TemporaryDirectory,
|
17 |
+
):
|
18 |
+
self.audio = audio
|
19 |
+
self.images = images
|
20 |
+
self.tmpdir = tmpdir
|
21 |
+
|
22 |
+
def __enter__(self):
|
23 |
+
return self
|
24 |
+
|
25 |
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
26 |
+
print("Cleaning up " + self.tmpdir.name)
|
27 |
+
self.tmpdir.cleanup()
|
28 |
+
|
29 |
+
|
30 |
+
def decode_input(input_path: PurePath, fps: int = 2) -> DecodedInput:
|
31 |
+
outdir = tempfile.TemporaryDirectory()
|
32 |
+
audio = PurePath(outdir.name) / "audio.mp3"
|
33 |
+
(
|
34 |
+
ffmpeg.input(
|
35 |
+
str(input_path),
|
36 |
+
)
|
37 |
+
.output(
|
38 |
+
str(audio),
|
39 |
+
loglevel="error",
|
40 |
+
**{
|
41 |
+
# Use 64k bitrate for smaller file
|
42 |
+
"b:a": "64k",
|
43 |
+
# Only output one channel, again for smaller file
|
44 |
+
"ac": "1",
|
45 |
+
},
|
46 |
+
)
|
47 |
+
.run()
|
48 |
+
)
|
49 |
+
(
|
50 |
+
ffmpeg.input(str(input_path))
|
51 |
+
.output(
|
52 |
+
str(PurePath(outdir.name) / "frame-%04d.jpg"),
|
53 |
+
loglevel="error",
|
54 |
+
**{
|
55 |
+
# Use fps as specified, scale image to fit within 512x512
|
56 |
+
"vf": f"fps={fps},scale='if(gt(iw,ih),512,-1)':'if(gt(ih,iw),512,-1)'",
|
57 |
+
"q:v": "20",
|
58 |
+
},
|
59 |
+
)
|
60 |
+
.run()
|
61 |
+
)
|
62 |
+
images = list(Path(outdir.name).glob("*.jpg"))
|
63 |
+
images.sort()
|
64 |
+
return DecodedInput(audio, tuple(images), outdir)
|
65 |
+
|
66 |
+
|
67 |
+
if __name__ == "__main__":
|
68 |
+
with decode_input(PurePath("data/question.mov")) as input:
|
69 |
+
print(input.audio)
|
70 |
+
print(input.images)
|
videoinput/package.json
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "multimodal",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "This is a simple script that takes a video file as input, and turns it into text plus images in a form GPT-4o can understand. The response from the model is then read aloud.",
|
5 |
+
"main": "dist/index.js",
|
6 |
+
"scripts": {
|
7 |
+
"test": "echo \"Error: no test specified\" && exit 1",
|
8 |
+
"build": "esbuild srcts/index.ts --bundle --platform=node --outfile=dist/index.js",
|
9 |
+
"watch": "esbuild srcts/index.ts --bundle --platform=node --outfile=dist/index.js --watch",
|
10 |
+
"typecheck": "tsc --noEmit"
|
11 |
+
},
|
12 |
+
"repository": {
|
13 |
+
"type": "git",
|
14 |
+
"url": "git+https://github.com/jcheng5/multimodal.git"
|
15 |
+
},
|
16 |
+
"author": "",
|
17 |
+
"license": "MIT",
|
18 |
+
"bugs": {
|
19 |
+
"url": "https://github.com/jcheng5/multimodal/issues"
|
20 |
+
},
|
21 |
+
"homepage": "https://github.com/jcheng5/multimodal#readme",
|
22 |
+
"dependencies": {},
|
23 |
+
"devDependencies": {
|
24 |
+
"@types/jquery": "^3.5.30",
|
25 |
+
"@types/rstudio-shiny": "https://github.com/rstudio/shiny#v1.7.0",
|
26 |
+
"esbuild": "^0.21.3",
|
27 |
+
"typescript": "^5.4.5"
|
28 |
+
}
|
29 |
+
}
|
videoinput/query.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Callable, Optional
|
2 |
+
|
3 |
+
import dotenv
|
4 |
+
from openai import AsyncOpenAI
|
5 |
+
|
6 |
+
from .input import decode_input
|
7 |
+
from .utils import NamedTemporaryFile, file_to_data_uri, timed
|
8 |
+
|
9 |
+
# Load OpenAI API key from .env file
|
10 |
+
dotenv.load_dotenv()
|
11 |
+
|
12 |
+
TERSE_PROMPT = """
|
13 |
+
The user you're responding to is EXTREMELY busy and cannot waste a single
|
14 |
+
second. Above all, answers must be as concise as possible. Every wasted word
|
15 |
+
will result in a huge deduction of points. In fact, use the absolute minimum
|
16 |
+
number of words while still technically answering the question. Avoid
|
17 |
+
adjectives, adverbs, fill words, and qualifiers.
|
18 |
+
"""
|
19 |
+
|
20 |
+
EXTRA_TERSE_PROMPT = """
|
21 |
+
Definitely don't restate any part of the question in your answer, if it can
|
22 |
+
possibly be avoided. Don't speak in complete sentences. Just get to the point as
|
23 |
+
quickly as possible.
|
24 |
+
"""
|
25 |
+
|
26 |
+
SUBJECT_PROMPT = """
|
27 |
+
If the user refers to "I" or "me" in the text input, you should assume that's
|
28 |
+
referring to the most prominent person in the video.
|
29 |
+
|
30 |
+
If the user refers to "you" in the text input, you should assume that's
|
31 |
+
referring to you, the AI model.
|
32 |
+
"""
|
33 |
+
|
34 |
+
VIDEO_PROMPT = """
|
35 |
+
The images are frames of a video at 2 frames per second. The user doesn't know
|
36 |
+
the video is split into frames, so make sure your video refers to these images
|
37 |
+
collectively as "the video", not "the images" or "the video frames".
|
38 |
+
"""
|
39 |
+
|
40 |
+
SPEAKING_PROMPT = """
|
41 |
+
The user is asking you to speak the answer. Make sure your response is in the
|
42 |
+
form of a friendly, casual spoken answer, not a formal written one.
|
43 |
+
"""
|
44 |
+
|
45 |
+
SYSTEM_PROMPT = (
|
46 |
+
VIDEO_PROMPT
|
47 |
+
+ SUBJECT_PROMPT
|
48 |
+
+ SPEAKING_PROMPT
|
49 |
+
# + TERSE_PROMPT
|
50 |
+
# + EXTRA_TERSE_PROMPT
|
51 |
+
)
|
52 |
+
|
53 |
+
|
54 |
+
async def process_video(
|
55 |
+
client: AsyncOpenAI, filepath: str, callback: Optional[Callable[[str], None]]
|
56 |
+
) -> None:
|
57 |
+
if callback is None:
|
58 |
+
callback = lambda _: None
|
59 |
+
|
60 |
+
callback("Decoding input")
|
61 |
+
input = decode_input(filepath, fps=2)
|
62 |
+
|
63 |
+
with input:
|
64 |
+
callback("Decoding speech")
|
65 |
+
with open(str(input.audio), "rb") as audio_file:
|
66 |
+
transcription = await client.audio.transcriptions.create(
|
67 |
+
model="whisper-1", file=audio_file
|
68 |
+
)
|
69 |
+
|
70 |
+
callback("Processing video")
|
71 |
+
images = [file_to_data_uri(filename, "image/jpeg") for filename in input.images]
|
72 |
+
|
73 |
+
callback("Querying")
|
74 |
+
response = await client.chat.completions.create(
|
75 |
+
model="gpt-4o",
|
76 |
+
messages=[
|
77 |
+
{
|
78 |
+
"role": "user",
|
79 |
+
"content": [
|
80 |
+
{"type": "text", "text": transcription.text},
|
81 |
+
*[
|
82 |
+
{
|
83 |
+
"type": "image_url",
|
84 |
+
"image_url": {"url": image, "detail": "auto"},
|
85 |
+
}
|
86 |
+
for image in images
|
87 |
+
],
|
88 |
+
],
|
89 |
+
},
|
90 |
+
{
|
91 |
+
"role": "system",
|
92 |
+
"content": [
|
93 |
+
{
|
94 |
+
"type": "text",
|
95 |
+
"text": SYSTEM_PROMPT,
|
96 |
+
}
|
97 |
+
],
|
98 |
+
},
|
99 |
+
],
|
100 |
+
)
|
101 |
+
|
102 |
+
callback("Converting to speech")
|
103 |
+
audio = await client.audio.speech.create(
|
104 |
+
model="tts-1",
|
105 |
+
voice="nova",
|
106 |
+
input=response.choices[0].message.content,
|
107 |
+
response_format="mp3",
|
108 |
+
)
|
109 |
+
|
110 |
+
callback("Encoding audio")
|
111 |
+
with NamedTemporaryFile(suffix=".mp3", delete_on_close=False) as file:
|
112 |
+
file.close()
|
113 |
+
audio.write_to_file(file.name)
|
114 |
+
return file_to_data_uri(file.name, "audio/mpeg")
|
videoinput/srcts/audioSpinner.ts
ADDED
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class AudioSpinnerElement extends HTMLElement {
|
2 |
+
#audio!: HTMLAudioElement;
|
3 |
+
#canvas!: HTMLCanvasElement;
|
4 |
+
#ctx2d!: CanvasRenderingContext2D;
|
5 |
+
#analyzer!: AnalyserNode;
|
6 |
+
#dataArray!: Float32Array;
|
7 |
+
#smoother!: Smoother<Float32Array>;
|
8 |
+
|
9 |
+
constructor() {
|
10 |
+
super();
|
11 |
+
this.attachShadow({ mode: "open" });
|
12 |
+
this.shadowRoot!.innerHTML = `
|
13 |
+
<style>
|
14 |
+
:host {
|
15 |
+
display: block;
|
16 |
+
position: relative;
|
17 |
+
}
|
18 |
+
::slotted(canvas) {
|
19 |
+
position: absolute;
|
20 |
+
top: 0;
|
21 |
+
left: 0;
|
22 |
+
}
|
23 |
+
::slotted(audio) {
|
24 |
+
display: none;
|
25 |
+
}
|
26 |
+
</style>
|
27 |
+
<slot name="audio"></slot>
|
28 |
+
<slot name="canvas"></slot>
|
29 |
+
`;
|
30 |
+
}
|
31 |
+
|
32 |
+
connectedCallback() {
|
33 |
+
// Create <audio>. This will play the sound.
|
34 |
+
const audioSlot = this.shadowRoot!.querySelector(
|
35 |
+
"slot[name=audio]"
|
36 |
+
)! as HTMLSlotElement;
|
37 |
+
this.#audio = this.ownerDocument.createElement("audio");
|
38 |
+
this.#audio.autoplay = true;
|
39 |
+
this.#audio.controls = false;
|
40 |
+
this.#audio.src = this.getAttribute("src")!;
|
41 |
+
this.#audio.slot = "audio";
|
42 |
+
audioSlot.assign(this.#audio);
|
43 |
+
this.#audio.addEventListener("play", () => {
|
44 |
+
this.#draw();
|
45 |
+
});
|
46 |
+
this.#audio.onpause = () => {
|
47 |
+
this.style.transition = "opacity 0.5s 1s";
|
48 |
+
this.classList.add("fade");
|
49 |
+
this.addEventListener("transitionend", () => {
|
50 |
+
this.remove();
|
51 |
+
});
|
52 |
+
};
|
53 |
+
|
54 |
+
// Create <canvas>. This will be the target of our vizualization.
|
55 |
+
const canvasSlot = this.shadowRoot!.querySelector(
|
56 |
+
"slot[name=canvas]"
|
57 |
+
)! as HTMLSlotElement;
|
58 |
+
this.#canvas = this.ownerDocument.createElement("canvas");
|
59 |
+
this.#canvas.slot = "canvas";
|
60 |
+
this.#canvas.width = this.clientWidth * window.devicePixelRatio;
|
61 |
+
this.#canvas.height = this.clientHeight * window.devicePixelRatio;
|
62 |
+
this.#canvas.style.width = this.clientWidth + "px";
|
63 |
+
this.#canvas.style.height = this.clientHeight + "px";
|
64 |
+
this.appendChild(this.#canvas);
|
65 |
+
canvasSlot.assign(this.#canvas);
|
66 |
+
this.#ctx2d = this.#canvas.getContext("2d")!;
|
67 |
+
this.#ctx2d.scale(window.devicePixelRatio, window.devicePixelRatio);
|
68 |
+
new ResizeObserver(() => {
|
69 |
+
this.#canvas.width = this.clientWidth;
|
70 |
+
this.#canvas.height = this.clientHeight;
|
71 |
+
}).observe(this);
|
72 |
+
|
73 |
+
// Initialize analyzer
|
74 |
+
const audioCtx = new AudioContext();
|
75 |
+
const source = audioCtx.createMediaElementSource(this.#audio);
|
76 |
+
this.#analyzer = new AnalyserNode(audioCtx, {
|
77 |
+
fftSize: 2048,
|
78 |
+
});
|
79 |
+
this.#dataArray = new Float32Array(this.#analyzer.frequencyBinCount);
|
80 |
+
source.connect(this.#analyzer);
|
81 |
+
this.#analyzer.connect(audioCtx.destination);
|
82 |
+
|
83 |
+
// Initialize persistent data structures needed for vizualization
|
84 |
+
const dataArray2 = new Float32Array(this.#analyzer.frequencyBinCount);
|
85 |
+
this.#smoother = new Smoother<Float32Array>(5, (samples) => {
|
86 |
+
for (let i = 0; i < dataArray2.length; i++) {
|
87 |
+
dataArray2[i] = 0;
|
88 |
+
for (let j = 0; j < samples.length; j++) {
|
89 |
+
dataArray2[i] += samples[j][i];
|
90 |
+
}
|
91 |
+
dataArray2[i] /= samples.length;
|
92 |
+
}
|
93 |
+
return dataArray2;
|
94 |
+
});
|
95 |
+
|
96 |
+
this.#draw();
|
97 |
+
}
|
98 |
+
|
99 |
+
#draw() {
|
100 |
+
if (!this.isConnected) {
|
101 |
+
return;
|
102 |
+
}
|
103 |
+
|
104 |
+
requestAnimationFrame(() => this.#draw());
|
105 |
+
|
106 |
+
const width = this.#canvas.width;
|
107 |
+
const height = this.#canvas.height;
|
108 |
+
this.#ctx2d.clearRect(0, 0, width, height);
|
109 |
+
|
110 |
+
this.#analyzer.getFloatTimeDomainData(this.#dataArray);
|
111 |
+
const smoothed = this.#smoother.add(new Float32Array(this.#dataArray));
|
112 |
+
|
113 |
+
const {
|
114 |
+
spinVelocity,
|
115 |
+
gap,
|
116 |
+
thickness,
|
117 |
+
minRadius,
|
118 |
+
radiusFactor,
|
119 |
+
steps,
|
120 |
+
blades,
|
121 |
+
} = this.#getSettings(width, height);
|
122 |
+
|
123 |
+
const avg =
|
124 |
+
(smoothed.reduce((a, b) => a + Math.abs(b), 0) / smoothed.length) * 4;
|
125 |
+
|
126 |
+
const radius = minRadius + (avg * (height - minRadius)) / radiusFactor;
|
127 |
+
for (let step = 0; step < steps; step++) {
|
128 |
+
const this_radius = radius - step * (radius / (steps + 1));
|
129 |
+
if (step === steps - 1) {
|
130 |
+
this.#drawPie(width, height, 0, Math.PI * 2, this_radius, thickness);
|
131 |
+
} else {
|
132 |
+
const seconds = new Date().getTime() / 1000;
|
133 |
+
const startAngle = (seconds * spinVelocity) % (Math.PI * 2);
|
134 |
+
for (let blade = 0; blade < blades; blade++) {
|
135 |
+
const angleOffset = ((Math.PI * 2) / blades) * blade;
|
136 |
+
const sweep = (Math.PI * 2) / blades - gap;
|
137 |
+
this.#drawPie(
|
138 |
+
width,
|
139 |
+
height,
|
140 |
+
startAngle + angleOffset,
|
141 |
+
sweep,
|
142 |
+
this_radius,
|
143 |
+
thickness
|
144 |
+
);
|
145 |
+
}
|
146 |
+
}
|
147 |
+
}
|
148 |
+
}
|
149 |
+
|
150 |
+
#drawPie(
|
151 |
+
width: number,
|
152 |
+
height: number,
|
153 |
+
startAngle: number,
|
154 |
+
sweep: number,
|
155 |
+
radius: number,
|
156 |
+
thickness?: number
|
157 |
+
) {
|
158 |
+
this.#ctx2d.beginPath();
|
159 |
+
this.#ctx2d.fillStyle = this.#canvas
|
160 |
+
.computedStyleMap()
|
161 |
+
.get("color")
|
162 |
+
?.toString()!;
|
163 |
+
if (!thickness) {
|
164 |
+
this.#ctx2d.moveTo(width / 2, height / 2);
|
165 |
+
}
|
166 |
+
this.#ctx2d.arc(
|
167 |
+
width / 2,
|
168 |
+
height / 2,
|
169 |
+
radius,
|
170 |
+
startAngle,
|
171 |
+
startAngle + sweep
|
172 |
+
);
|
173 |
+
if (!thickness) {
|
174 |
+
this.#ctx2d.lineTo(width / 2, height / 2);
|
175 |
+
} else {
|
176 |
+
this.#ctx2d.arc(
|
177 |
+
width / 2,
|
178 |
+
height / 2,
|
179 |
+
radius - thickness,
|
180 |
+
startAngle + sweep,
|
181 |
+
startAngle,
|
182 |
+
true
|
183 |
+
);
|
184 |
+
}
|
185 |
+
this.#ctx2d.fill();
|
186 |
+
}
|
187 |
+
|
188 |
+
#getSettings(width: number, height: number) {
|
189 |
+
// Visualization settings
|
190 |
+
const settings = {
|
191 |
+
spinVelocity: 5,
|
192 |
+
gap: Math.PI / 5,
|
193 |
+
thickness: 2.5,
|
194 |
+
minRadius: Math.min(width, height) / 6,
|
195 |
+
radiusFactor: 1.8,
|
196 |
+
steps: 3,
|
197 |
+
blades: 3,
|
198 |
+
};
|
199 |
+
for (const key in settings) {
|
200 |
+
const value = tryParseFloat(this.dataset[key]);
|
201 |
+
if (typeof value !== "undefined") {
|
202 |
+
Object.assign(settings, { [key]: value });
|
203 |
+
}
|
204 |
+
}
|
205 |
+
return settings;
|
206 |
+
}
|
207 |
+
}
|
208 |
+
|
209 |
+
window.customElements.define("audio-spinner", AudioSpinnerElement);
|
210 |
+
|
211 |
+
class Smoother<T> {
|
212 |
+
#samples: T[] = [];
|
213 |
+
#smooth: (samples: T[]) => T;
|
214 |
+
#size: number;
|
215 |
+
#pos: number;
|
216 |
+
|
217 |
+
constructor(size: number, smooth: (samples: T[]) => T) {
|
218 |
+
this.#size = size;
|
219 |
+
this.#pos = 0;
|
220 |
+
this.#smooth = smooth;
|
221 |
+
}
|
222 |
+
|
223 |
+
add(sample: T): T {
|
224 |
+
this.#samples[this.#pos] = sample;
|
225 |
+
this.#pos = (this.#pos + 1) % this.#size;
|
226 |
+
return this.smoothed();
|
227 |
+
}
|
228 |
+
|
229 |
+
smoothed(): T {
|
230 |
+
return this.#smooth(this.#samples);
|
231 |
+
}
|
232 |
+
}
|
233 |
+
|
234 |
+
function tryParseFloat(str?: string): number | undefined {
|
235 |
+
if (typeof str === "undefined") {
|
236 |
+
return undefined;
|
237 |
+
}
|
238 |
+
const parsed = parseFloat(str);
|
239 |
+
return isNaN(parsed) ? undefined : parsed;
|
240 |
+
}
|
videoinput/srcts/avSettingsMenu.ts
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class DeviceChangeEvent extends CustomEvent<{ deviceId: string | null }> {
|
2 |
+
constructor(type: string, detail: { deviceId: string | null }) {
|
3 |
+
super(type, { detail });
|
4 |
+
}
|
5 |
+
}
|
6 |
+
|
7 |
+
class AVSettingsMenuElement extends HTMLElement {
|
8 |
+
constructor() {
|
9 |
+
super();
|
10 |
+
this.addEventListener("click", (e) => {
|
11 |
+
if (e.target instanceof HTMLAnchorElement) {
|
12 |
+
const a = e.target;
|
13 |
+
if (a.classList.contains("camera-device-item")) {
|
14 |
+
this.cameraId = a.dataset.deviceId!;
|
15 |
+
} else if (a.classList.contains("mic-device-item")) {
|
16 |
+
this.micId = a.dataset.deviceId!;
|
17 |
+
}
|
18 |
+
}
|
19 |
+
});
|
20 |
+
}
|
21 |
+
|
22 |
+
#setDevices(deviceType: "camera" | "mic", devices: MediaDeviceInfo[]) {
|
23 |
+
const deviceEls = devices.map((dev) =>
|
24 |
+
this.#createDeviceElement(dev, `${deviceType}-device-item`)
|
25 |
+
);
|
26 |
+
const header = this.querySelector(`.${deviceType}-header`)!;
|
27 |
+
header.after(...deviceEls);
|
28 |
+
}
|
29 |
+
|
30 |
+
setCameras(cameras: MediaDeviceInfo[]) {
|
31 |
+
this.#setDevices("camera", cameras);
|
32 |
+
}
|
33 |
+
|
34 |
+
setMics(mics: MediaDeviceInfo[]) {
|
35 |
+
this.#setDevices("mic", mics);
|
36 |
+
}
|
37 |
+
|
38 |
+
get cameraId(): string | null {
|
39 |
+
return this.#getSelectedDevice("camera");
|
40 |
+
}
|
41 |
+
|
42 |
+
set cameraId(id: string | null) {
|
43 |
+
this.#setSelectedDevice("camera", id);
|
44 |
+
}
|
45 |
+
|
46 |
+
get micId(): string | null {
|
47 |
+
return this.#getSelectedDevice("mic");
|
48 |
+
}
|
49 |
+
|
50 |
+
set micId(id: string | null) {
|
51 |
+
this.#setSelectedDevice("mic", id);
|
52 |
+
}
|
53 |
+
|
54 |
+
#createDeviceElement(dev: MediaDeviceInfo, className: string): HTMLLIElement {
|
55 |
+
const li = this.ownerDocument.createElement("li");
|
56 |
+
const a = li.appendChild(this.ownerDocument.createElement("a"));
|
57 |
+
a.onclick = (e) => e.preventDefault();
|
58 |
+
a.href = "#";
|
59 |
+
a.textContent = dev.label;
|
60 |
+
a.dataset.deviceId = dev.deviceId!;
|
61 |
+
a.className = className;
|
62 |
+
return li;
|
63 |
+
}
|
64 |
+
|
65 |
+
#getSelectedDevice(device: "camera" | "mic"): string | null {
|
66 |
+
return (
|
67 |
+
(
|
68 |
+
this.querySelector(
|
69 |
+
`a.${device}-device-item.active`
|
70 |
+
) as HTMLAnchorElement
|
71 |
+
)?.dataset.deviceId ?? null
|
72 |
+
);
|
73 |
+
}
|
74 |
+
|
75 |
+
#setSelectedDevice(device: "camera" | "mic", id: string | null) {
|
76 |
+
this.querySelectorAll(`a.${device}-device-item.active`).forEach((a) =>
|
77 |
+
a.classList.remove("active")
|
78 |
+
);
|
79 |
+
if (id) {
|
80 |
+
this.querySelector(
|
81 |
+
`a.${device}-device-item[data-device-id="${id}"]`
|
82 |
+
)!.classList.add("active");
|
83 |
+
}
|
84 |
+
this.dispatchEvent(
|
85 |
+
new DeviceChangeEvent(`${device}-change`, {
|
86 |
+
deviceId: id,
|
87 |
+
})
|
88 |
+
);
|
89 |
+
}
|
90 |
+
}
|
91 |
+
customElements.define("av-settings-menu", AVSettingsMenuElement);
|
videoinput/srcts/index.ts
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { BindScope } from "rstudio-shiny/srcts/types/src/shiny/bind";
|
2 |
+
|
3 |
+
// Register custom elements
|
4 |
+
import "./videoClipper";
|
5 |
+
import "./avSettingsMenu";
|
6 |
+
import "./audioSpinner";
|
7 |
+
|
8 |
+
// Create input binding to send video clips from <video-clipper> to Shiny
|
9 |
+
class VideoClipperBinding extends Shiny.InputBinding {
|
10 |
+
#lastKnownValue = new WeakMap<HTMLElement, unknown>();
|
11 |
+
#handlers = new WeakMap<HTMLElement, (ev: Event) => Promise<void>>();
|
12 |
+
|
13 |
+
find(scope: BindScope): JQuery<HTMLElement> {
|
14 |
+
return $(scope).find("video-clipper");
|
15 |
+
}
|
16 |
+
|
17 |
+
getValue(el: HTMLElement): unknown {
|
18 |
+
return this.#lastKnownValue.get(el);
|
19 |
+
}
|
20 |
+
|
21 |
+
subscribe(el: HTMLElement, callback: (value: boolean) => void): void {
|
22 |
+
const handler = async (ev: Event) => {
|
23 |
+
const blob = (ev as BlobEvent).data;
|
24 |
+
this.#lastKnownValue.set(el, {
|
25 |
+
type: blob.type,
|
26 |
+
bytes: await base64(blob),
|
27 |
+
});
|
28 |
+
callback(true);
|
29 |
+
};
|
30 |
+
el.addEventListener("data", handler);
|
31 |
+
this.#handlers.set(el, handler);
|
32 |
+
}
|
33 |
+
|
34 |
+
unsubscribe(el: HTMLElement): void {
|
35 |
+
const handler = this.#handlers.get(el)!;
|
36 |
+
el.removeEventListener("data", handler);
|
37 |
+
this.#handlers.delete(el);
|
38 |
+
}
|
39 |
+
}
|
40 |
+
|
41 |
+
window.Shiny.inputBindings.register(new VideoClipperBinding(), "video-clipper");
|
42 |
+
|
43 |
+
/**
|
44 |
+
* Encode a Blob as a base64 string
|
45 |
+
* @param blob The Blob to encode
|
46 |
+
* @returns A base64-encoded string
|
47 |
+
*/
|
48 |
+
async function base64(blob: Blob): Promise<string> {
|
49 |
+
const buf = await blob.arrayBuffer();
|
50 |
+
const results = [];
|
51 |
+
const CHUNKSIZE = 1024;
|
52 |
+
for (let i = 0; i < buf.byteLength; i += CHUNKSIZE) {
|
53 |
+
const chunk = buf.slice(i, i + CHUNKSIZE);
|
54 |
+
results.push(String.fromCharCode(...new Uint8Array(chunk)));
|
55 |
+
}
|
56 |
+
return btoa(results.join(""));
|
57 |
+
}
|
videoinput/srcts/videoClipper.ts
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class VideoClipperElement extends HTMLElement {
|
2 |
+
video: HTMLVideoElement;
|
3 |
+
avSettingsMenu!: AVSettingsMenuElement;
|
4 |
+
buttonRecord!: HTMLButtonElement;
|
5 |
+
buttonStop!: HTMLButtonElement;
|
6 |
+
|
7 |
+
cameraStream?: MediaStream;
|
8 |
+
micStream?: MediaStream;
|
9 |
+
|
10 |
+
recorder?: MediaRecorder;
|
11 |
+
chunks: Blob[] = [];
|
12 |
+
|
13 |
+
constructor() {
|
14 |
+
super();
|
15 |
+
this.attachShadow({ mode: "open" });
|
16 |
+
this.shadowRoot!.innerHTML = `
|
17 |
+
<style>
|
18 |
+
:host {
|
19 |
+
display: grid;
|
20 |
+
grid-template-rows: 1fr;
|
21 |
+
grid-template-columns: 1fr;
|
22 |
+
width: 100%;
|
23 |
+
height: min-content;
|
24 |
+
}
|
25 |
+
video {
|
26 |
+
grid-column: 1 / 2;
|
27 |
+
grid-row: 1 / 2;
|
28 |
+
width: 100%;
|
29 |
+
object-fit: cover;
|
30 |
+
background-color: var(--video-clip-bg, black);
|
31 |
+
aspect-ratio: 16 / 9;
|
32 |
+
border-radius: var(--video-clip-border-radius, var(--bs-border-radius-lg));
|
33 |
+
}
|
34 |
+
video.mirrored {
|
35 |
+
transform: scaleX(-1);
|
36 |
+
}
|
37 |
+
.panel-settings {
|
38 |
+
grid-column: 1 / 2;
|
39 |
+
grid-row: 1 / 2;
|
40 |
+
justify-self: end;
|
41 |
+
margin: 0.5em;
|
42 |
+
}
|
43 |
+
.panel-buttons {
|
44 |
+
grid-column: 1 / 2;
|
45 |
+
grid-row: 1 / 2;
|
46 |
+
justify-self: end;
|
47 |
+
align-self: end;
|
48 |
+
margin: 0.5em;
|
49 |
+
}
|
50 |
+
</style>
|
51 |
+
<video part="video" muted></video>
|
52 |
+
<div class="panel-settings">
|
53 |
+
<slot name="settings"></slot>
|
54 |
+
</div>
|
55 |
+
<div class="panel-buttons">
|
56 |
+
<slot name="recording-controls"></slot>
|
57 |
+
</div>
|
58 |
+
`;
|
59 |
+
this.video = this.shadowRoot!.querySelector("video")!;
|
60 |
+
}
|
61 |
+
connectedCallback() {
|
62 |
+
(async () => {
|
63 |
+
const slotSettings = this.shadowRoot!.querySelector(
|
64 |
+
"slot[name=settings]"
|
65 |
+
)! as HTMLSlotElement;
|
66 |
+
slotSettings.addEventListener("slotchange", async () => {
|
67 |
+
this.avSettingsMenu =
|
68 |
+
slotSettings.assignedElements()[0] as AVSettingsMenuElement;
|
69 |
+
await this.#initializeMediaInput();
|
70 |
+
if (this.buttonRecord) {
|
71 |
+
this.#setEnabledButton(this.buttonRecord);
|
72 |
+
}
|
73 |
+
});
|
74 |
+
|
75 |
+
const slotControls = this.shadowRoot!.querySelector(
|
76 |
+
"slot[name=recording-controls]"
|
77 |
+
)! as HTMLSlotElement;
|
78 |
+
slotControls.addEventListener("slotchange", () => {
|
79 |
+
const findButton = (selector: string): HTMLElement | null => {
|
80 |
+
for (const el of slotControls.assignedElements()) {
|
81 |
+
if (el.matches(selector)) {
|
82 |
+
return el as HTMLElement;
|
83 |
+
}
|
84 |
+
const sub = el.querySelector(selector);
|
85 |
+
if (sub) {
|
86 |
+
return sub as HTMLElement;
|
87 |
+
}
|
88 |
+
}
|
89 |
+
return null;
|
90 |
+
};
|
91 |
+
this.buttonRecord = findButton(".record-button")! as HTMLButtonElement;
|
92 |
+
this.buttonStop = findButton(".stop-button")! as HTMLButtonElement;
|
93 |
+
|
94 |
+
this.#setEnabledButton();
|
95 |
+
|
96 |
+
this.buttonRecord.addEventListener("click", () => {
|
97 |
+
this.#setEnabledButton(this.buttonStop);
|
98 |
+
this._beginRecord();
|
99 |
+
});
|
100 |
+
this.buttonStop.addEventListener("click", () => {
|
101 |
+
this._endRecord();
|
102 |
+
this.#setEnabledButton(this.buttonRecord);
|
103 |
+
});
|
104 |
+
});
|
105 |
+
})().catch((err) => {
|
106 |
+
console.error(err);
|
107 |
+
});
|
108 |
+
}
|
109 |
+
|
110 |
+
disconnectedCallback() {}
|
111 |
+
|
112 |
+
#setEnabledButton(btn?: HTMLButtonElement) {
|
113 |
+
this.buttonRecord.style.display =
|
114 |
+
btn === this.buttonRecord ? "inline-block" : "none";
|
115 |
+
this.buttonStop.style.display =
|
116 |
+
btn === this.buttonStop ? "inline-block" : "none";
|
117 |
+
}
|
118 |
+
|
119 |
+
async setMediaDevices(
|
120 |
+
cameraId: string | null,
|
121 |
+
micId: string | null
|
122 |
+
): Promise<{ cameraId: string; micId: string }> {
|
123 |
+
if (this.cameraStream) {
|
124 |
+
this.cameraStream.getTracks().forEach((track) => track.stop());
|
125 |
+
}
|
126 |
+
|
127 |
+
this.cameraStream = await navigator.mediaDevices.getUserMedia({
|
128 |
+
video: {
|
129 |
+
deviceId: cameraId || undefined,
|
130 |
+
facingMode: "user",
|
131 |
+
aspectRatio: 16 / 9,
|
132 |
+
},
|
133 |
+
audio: {
|
134 |
+
deviceId: micId || undefined,
|
135 |
+
},
|
136 |
+
});
|
137 |
+
|
138 |
+
// TODO: I can't figure out how to tell if this is actually a selfie cam.
|
139 |
+
// Ideally we wouldn't mirror unless we are sure.
|
140 |
+
const isSelfieCam = true; // this.cameraStream.getVideoTracks()[0].getSettings().facingMode === "user";
|
141 |
+
this.video.classList.toggle("mirrored", isSelfieCam);
|
142 |
+
|
143 |
+
/* Prevent the height from jumping around when switching cameras */
|
144 |
+
const aspectRatio = this.cameraStream
|
145 |
+
.getVideoTracks()[0]
|
146 |
+
.getSettings().aspectRatio;
|
147 |
+
if (aspectRatio) {
|
148 |
+
this.video.style.aspectRatio = `${aspectRatio}`;
|
149 |
+
} else {
|
150 |
+
this.video.style.aspectRatio = "";
|
151 |
+
}
|
152 |
+
this.video.srcObject = this.cameraStream!;
|
153 |
+
this.video.play();
|
154 |
+
|
155 |
+
return {
|
156 |
+
cameraId: this.cameraStream.getVideoTracks()[0].getSettings().deviceId!,
|
157 |
+
micId: this.cameraStream.getAudioTracks()[0].getSettings().deviceId!,
|
158 |
+
};
|
159 |
+
}
|
160 |
+
|
161 |
+
async #initializeMediaInput() {
|
162 |
+
// Retrieve the user's previous camera and mic settings, if they ever
|
163 |
+
// explicitly chose one
|
164 |
+
const savedCamera = window.localStorage.getItem("multimodal-camera");
|
165 |
+
const savedMic = window.localStorage.getItem("multimodal-mic");
|
166 |
+
|
167 |
+
// Initialize the camera and mic with the saved settings. It's important to
|
168 |
+
// request camera/mic access _before_ we attempt to enumerate devices,
|
169 |
+
// because if the user has not granted camera/mic access, enumerateDevices()
|
170 |
+
// will not prompt the user for permission and will instead return empty
|
171 |
+
// devices.
|
172 |
+
//
|
173 |
+
// The return values are the actual camera and mic IDs that were used, which
|
174 |
+
// may be different from the saved values if those devices are no longer
|
175 |
+
// available.
|
176 |
+
const { cameraId, micId } = await this.setMediaDevices(
|
177 |
+
savedCamera,
|
178 |
+
savedMic
|
179 |
+
);
|
180 |
+
|
181 |
+
// Populate the camera and mic dropdowns with the available devices
|
182 |
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
183 |
+
this.avSettingsMenu.setCameras(
|
184 |
+
devices.filter((dev) => dev.kind === "videoinput")
|
185 |
+
);
|
186 |
+
this.avSettingsMenu.setMics(
|
187 |
+
devices.filter((dev) => dev.kind === "audioinput")
|
188 |
+
);
|
189 |
+
|
190 |
+
// Update the dropdown UI to reflect the actual devices that were used
|
191 |
+
this.avSettingsMenu.cameraId = cameraId;
|
192 |
+
this.avSettingsMenu.micId = micId;
|
193 |
+
|
194 |
+
// Listen for changes to the camera and mic dropdowns
|
195 |
+
const handleDeviceChange = async (
|
196 |
+
deviceType: string,
|
197 |
+
deviceId: string | null
|
198 |
+
) => {
|
199 |
+
if (!deviceId) return;
|
200 |
+
window.localStorage.setItem(`multimodal-${deviceType}`, deviceId);
|
201 |
+
await this.setMediaDevices(
|
202 |
+
this.avSettingsMenu.cameraId,
|
203 |
+
this.avSettingsMenu.micId
|
204 |
+
);
|
205 |
+
};
|
206 |
+
this.avSettingsMenu.addEventListener("camera-change", (e) => {
|
207 |
+
handleDeviceChange("camera", this.avSettingsMenu.cameraId);
|
208 |
+
});
|
209 |
+
this.avSettingsMenu.addEventListener("mic-change", (e) => {
|
210 |
+
handleDeviceChange("mic", this.avSettingsMenu.micId);
|
211 |
+
});
|
212 |
+
}
|
213 |
+
|
214 |
+
_beginRecord() {
|
215 |
+
// Create a MediaRecorder object
|
216 |
+
this.recorder = new MediaRecorder(this.cameraStream!, {});
|
217 |
+
|
218 |
+
this.recorder.addEventListener("error", (e) => {
|
219 |
+
console.error("MediaRecorder error:", (e as ErrorEvent).error);
|
220 |
+
});
|
221 |
+
this.recorder.addEventListener("dataavailable", (e) => {
|
222 |
+
// console.log("chunk: ", e.data.size, e.data.type);
|
223 |
+
this.chunks.push(e.data);
|
224 |
+
});
|
225 |
+
this.recorder.addEventListener("start", () => {
|
226 |
+
// console.log("Recording started");
|
227 |
+
});
|
228 |
+
this.recorder.start();
|
229 |
+
}
|
230 |
+
|
231 |
+
_endRecord(emit: boolean = true) {
|
232 |
+
this.recorder!.stop();
|
233 |
+
if (!emit) {
|
234 |
+
this.chunks = [];
|
235 |
+
} else {
|
236 |
+
// Use setTimeout to give it a moment to finish processing the last chunk
|
237 |
+
setTimeout(() => {
|
238 |
+
// console.log("chunks: ", this.chunks.length);
|
239 |
+
const blob = new Blob(this.chunks, { type: this.chunks[0].type });
|
240 |
+
|
241 |
+
// emit blobevent
|
242 |
+
const event = new BlobEvent("data", {
|
243 |
+
data: blob,
|
244 |
+
});
|
245 |
+
try {
|
246 |
+
this.dispatchEvent(event);
|
247 |
+
} finally {
|
248 |
+
this.chunks = [];
|
249 |
+
}
|
250 |
+
}, 0);
|
251 |
+
}
|
252 |
+
}
|
253 |
+
}
|
254 |
+
customElements.define("video-clipper", VideoClipperElement);
|
videoinput/tsconfig.json
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"outDir": "./dist",
|
4 |
+
"rootDir": "./srcts",
|
5 |
+
"target": "es2020",
|
6 |
+
"module": "commonjs",
|
7 |
+
"lib": ["dom", "es2020"],
|
8 |
+
"strict": true,
|
9 |
+
"esModuleInterop": true,
|
10 |
+
"skipLibCheck": true,
|
11 |
+
"forceConsistentCasingInFileNames": true
|
12 |
+
},
|
13 |
+
"include": ["srcts/**/*"]
|
14 |
+
}
|
videoinput/ui.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import math
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
from faicons import icon_svg
|
5 |
+
from htmltools import HTMLDependency
|
6 |
+
from shiny import module, ui
|
7 |
+
|
8 |
+
__all__ = (
|
9 |
+
"input_video_clip",
|
10 |
+
"audio_spinner",
|
11 |
+
)
|
12 |
+
|
13 |
+
multimodal_dep = HTMLDependency(
|
14 |
+
"multimodal",
|
15 |
+
"0.0.1",
|
16 |
+
source={
|
17 |
+
"subdir": str(Path(__file__).parent / "dist"),
|
18 |
+
},
|
19 |
+
script={"src": "index.js"},
|
20 |
+
stylesheet={"href": "index.css"},
|
21 |
+
)
|
22 |
+
|
23 |
+
|
24 |
+
def input_video_clip(id: str, **kwargs):
|
25 |
+
id = module.resolve_id(id)
|
26 |
+
|
27 |
+
return ui.Tag(
|
28 |
+
"video-clipper",
|
29 |
+
multimodal_dep,
|
30 |
+
ui.Tag(
|
31 |
+
"av-settings-menu",
|
32 |
+
ui.div(
|
33 |
+
ui.tags.button(
|
34 |
+
icon_svg("gear").add_class("fw"),
|
35 |
+
class_="btn btn-sm btn-secondary dropdown-toggle px-3 py-2",
|
36 |
+
type="button",
|
37 |
+
**{"data-bs-toggle": "dropdown"},
|
38 |
+
),
|
39 |
+
ui.tags.ul(
|
40 |
+
ui.tags.li(
|
41 |
+
ui.tags.h6("Camera", class_="dropdown-header"),
|
42 |
+
class_="camera-header",
|
43 |
+
),
|
44 |
+
# Camera items will go here
|
45 |
+
ui.tags.li(ui.tags.hr(class_="dropdown-divider")),
|
46 |
+
ui.tags.li(
|
47 |
+
ui.tags.h6("Microphone", class_="dropdown-header"),
|
48 |
+
class_="mic-header",
|
49 |
+
),
|
50 |
+
# Microphone items will go here
|
51 |
+
class_="dropdown-menu",
|
52 |
+
),
|
53 |
+
class_="btn-group",
|
54 |
+
),
|
55 |
+
slot="settings",
|
56 |
+
),
|
57 |
+
ui.div(
|
58 |
+
ui.tags.button(
|
59 |
+
ui.TagList(
|
60 |
+
ui.tags.div(
|
61 |
+
style="display: inline-block; background-color: red; width: 1rem; height: 1rem; border-radius: 100%; position: relative; top: 0.175rem; margin-right: 0.3rem;"
|
62 |
+
),
|
63 |
+
"Record",
|
64 |
+
),
|
65 |
+
style="display: block;",
|
66 |
+
class_="record-button btn btn-secondary px-3 mx-auto",
|
67 |
+
),
|
68 |
+
ui.tags.button(
|
69 |
+
ui.TagList(
|
70 |
+
ui.tags.div(
|
71 |
+
style="display: inline-block; background-color: currentColor; width: 1rem; height: 1rem; position: relative; top: 0.175rem; margin-right: 0.3rem;"
|
72 |
+
),
|
73 |
+
"Stop",
|
74 |
+
),
|
75 |
+
style="display: block;",
|
76 |
+
class_="stop-button btn btn-secondary px-3 mx-auto",
|
77 |
+
),
|
78 |
+
slot="recording-controls",
|
79 |
+
class_="btn-group",
|
80 |
+
**{"aria-label": "Recording controls"},
|
81 |
+
),
|
82 |
+
id=id,
|
83 |
+
**kwargs,
|
84 |
+
)
|
85 |
+
|
86 |
+
|
87 |
+
def audio_spinner(
|
88 |
+
*,
|
89 |
+
src: str,
|
90 |
+
spin_velocity: float = 1,
|
91 |
+
gap: float = math.pi / 5,
|
92 |
+
thickness: float = 2.5,
|
93 |
+
min_radius: float = 30,
|
94 |
+
radius_factor: float = 1.8,
|
95 |
+
steps: float = 3,
|
96 |
+
blades: float = 3,
|
97 |
+
**kwargs
|
98 |
+
):
|
99 |
+
return ui.Tag(
|
100 |
+
"audio-spinner",
|
101 |
+
multimodal_dep,
|
102 |
+
src=src,
|
103 |
+
style="width: 125px; height: 125px;",
|
104 |
+
class_="mx-auto",
|
105 |
+
**{
|
106 |
+
"data-spin-velocity": spin_velocity,
|
107 |
+
"data-gap": gap,
|
108 |
+
"data-thickness": thickness,
|
109 |
+
"data-min-radius": min_radius,
|
110 |
+
"data-radius-factor": radius_factor,
|
111 |
+
"data-steps": steps,
|
112 |
+
"data-blades": blades,
|
113 |
+
},
|
114 |
+
)
|
videoinput/utils.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import annotations
|
2 |
+
|
3 |
+
import base64
|
4 |
+
import contextlib
|
5 |
+
import os
|
6 |
+
import tempfile
|
7 |
+
import time
|
8 |
+
|
9 |
+
import colorama
|
10 |
+
|
11 |
+
__all__ = (
|
12 |
+
"timed",
|
13 |
+
"file_to_data_uri",
|
14 |
+
)
|
15 |
+
|
16 |
+
|
17 |
+
@contextlib.contextmanager
|
18 |
+
def timed(msg):
|
19 |
+
start = time.perf_counter()
|
20 |
+
print(colorama.Style.DIM + f"╔ {msg}" + colorama.Style.RESET_ALL)
|
21 |
+
yield
|
22 |
+
elapsed = time.perf_counter() - start
|
23 |
+
print(
|
24 |
+
colorama.Style.DIM + f"╚ Finished in {elapsed:.3f}s" + colorama.Style.RESET_ALL
|
25 |
+
)
|
26 |
+
|
27 |
+
|
28 |
+
def file_to_data_uri(file_path, mime_type):
|
29 |
+
with open(file_path, "rb") as file:
|
30 |
+
encoded_string = base64.b64encode(file.read()).decode("utf-8")
|
31 |
+
return f"data:{mime_type};base64,{encoded_string}"
|
32 |
+
|
33 |
+
|
34 |
+
class NamedTemporaryFile(contextlib.AbstractContextManager):
|
35 |
+
"""
|
36 |
+
tempfile.NamedTemporaryFile with an additional `delete_on_close` parameter.
|
37 |
+
|
38 |
+
The `delete_on_close` parameter was only added in Python 3.12, but we badly
|
39 |
+
need it on Windows: because file access on Windows is exclusive, we can't
|
40 |
+
write to and then read from a file without closing it in between. But
|
41 |
+
without `delete_on_close`, the file is deleted on close.
|
42 |
+
|
43 |
+
This class is a thin shim around tempfile.NamedTemporaryFile that adds the
|
44 |
+
parameter for older Python versions.
|
45 |
+
"""
|
46 |
+
|
47 |
+
def __init__(
|
48 |
+
self,
|
49 |
+
mode: str = "w+b",
|
50 |
+
buffering: int = -1,
|
51 |
+
encoding: str | None = None,
|
52 |
+
newline: str | None = None,
|
53 |
+
suffix: str = "",
|
54 |
+
prefix: str = "tmp",
|
55 |
+
dir: str | None = None,
|
56 |
+
delete: bool = True,
|
57 |
+
*,
|
58 |
+
errors: str | None = None,
|
59 |
+
delete_on_close: bool = True,
|
60 |
+
):
|
61 |
+
self._needs_manual_delete = delete and not delete_on_close
|
62 |
+
self._file = tempfile.NamedTemporaryFile(
|
63 |
+
mode=mode,
|
64 |
+
buffering=buffering,
|
65 |
+
encoding=encoding,
|
66 |
+
newline=newline,
|
67 |
+
suffix=suffix,
|
68 |
+
prefix=prefix,
|
69 |
+
dir=dir,
|
70 |
+
delete=delete and delete_on_close,
|
71 |
+
errors=errors,
|
72 |
+
)
|
73 |
+
|
74 |
+
def __enter__(self) -> tempfile.NamedTemporaryFile:
|
75 |
+
return self._file.__enter__()
|
76 |
+
|
77 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
78 |
+
self._file.__exit__(exc_type, exc_val, exc_tb)
|
79 |
+
if self._needs_manual_delete:
|
80 |
+
os.unlink(self._file.name)
|
videoinput/yarn.lock
ADDED
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
2 |
+
# yarn lockfile v1
|
3 |
+
|
4 |
+
|
5 |
+
"@esbuild/[email protected]":
|
6 |
+
version "0.21.3"
|
7 |
+
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.3.tgz#78d3e6dcd19c1cb91f3940143e86dad1094aee81"
|
8 |
+
integrity sha512-yTgnwQpFVYfvvo4SvRFB0SwrW8YjOxEoT7wfMT7Ol5v7v5LDNvSGo67aExmxOb87nQNeWPVvaGBNfQ7BXcrZ9w==
|
9 |
+
|
10 |
+
"@esbuild/[email protected]":
|
11 |
+
version "0.21.3"
|
12 |
+
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.3.tgz#5eea56c21d61734942e050840d881eb7bedc3993"
|
13 |
+
integrity sha512-c+ty9necz3zB1Y+d/N+mC6KVVkGUUOcm4ZmT5i/Fk5arOaY3i6CA3P5wo/7+XzV8cb4GrI/Zjp8NuOQ9Lfsosw==
|
14 |
+
|
15 |
+
"@esbuild/[email protected]":
|
16 |
+
version "0.21.3"
|
17 |
+
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.3.tgz#7fda92e3231043c071ea6aa76c92accea86439fd"
|
18 |
+
integrity sha512-bviJOLMgurLJtF1/mAoJLxDZDL6oU5/ztMHnJQRejbJrSc9FFu0QoUoFhvi6qSKJEw9y5oGyvr9fuDtzJ30rNQ==
|
19 |
+
|
20 |
+
"@esbuild/[email protected]":
|
21 |
+
version "0.21.3"
|
22 |
+
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.3.tgz#625d139bde81b81f54ff493b1381ca0f540200f3"
|
23 |
+
integrity sha512-JReHfYCRK3FVX4Ra+y5EBH1b9e16TV2OxrPAvzMsGeES0X2Ndm9ImQRI4Ket757vhc5XBOuGperw63upesclRw==
|
24 |
+
|
25 |
+
"@esbuild/[email protected]":
|
26 |
+
version "0.21.3"
|
27 |
+
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.3.tgz#fa25f38a43ff4c469589d1dc93448d534d7f313b"
|
28 |
+
integrity sha512-U3fuQ0xNiAkXOmQ6w5dKpEvXQRSpHOnbw7gEfHCRXPeTKW9sBzVck6C5Yneb8LfJm0l6le4NQfkNPnWMSlTFUQ==
|
29 |
+
|
30 |
+
"@esbuild/[email protected]":
|
31 |
+
version "0.21.3"
|
32 |
+
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.3.tgz#2e450b8214f179a56b4559b2f107060e2b792c7e"
|
33 |
+
integrity sha512-3m1CEB7F07s19wmaMNI2KANLcnaqryJxO1fXHUV5j1rWn+wMxdUYoPyO2TnAbfRZdi7ADRwJClmOwgT13qlP3Q==
|
34 |
+
|
35 |
+
"@esbuild/[email protected]":
|
36 |
+
version "0.21.3"
|
37 |
+
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.3.tgz#f6b29e07bce25c545f6f7bb031d3be6a6ea1dc50"
|
38 |
+
integrity sha512-fsNAAl5pU6wmKHq91cHWQT0Fz0vtyE1JauMzKotrwqIKAswwP5cpHUCxZNSTuA/JlqtScq20/5KZ+TxQdovU/g==
|
39 |
+
|
40 |
+
"@esbuild/[email protected]":
|
41 |
+
version "0.21.3"
|
42 |
+
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.3.tgz#1a5da2bf89f8d67102820d893d271a270ae55751"
|
43 |
+
integrity sha512-tci+UJ4zP5EGF4rp8XlZIdq1q1a/1h9XuronfxTMCNBslpCtmk97Q/5qqy1Mu4zIc0yswN/yP/BLX+NTUC1bXA==
|
44 |
+
|
45 |
+
"@esbuild/[email protected]":
|
46 |
+
version "0.21.3"
|
47 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.3.tgz#355f6624c1ac6f5f68841a327ac90b98c679626c"
|
48 |
+
integrity sha512-vvG6R5g5ieB4eCJBQevyDMb31LMHthLpXTc2IGkFnPWS/GzIFDnaYFp558O+XybTmYrVjxnryru7QRleJvmZ6Q==
|
49 |
+
|
50 |
+
"@esbuild/[email protected]":
|
51 |
+
version "0.21.3"
|
52 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.3.tgz#872a476ca18a962a98700024c447a79279db1d45"
|
53 |
+
integrity sha512-f6kz2QpSuyHHg01cDawj0vkyMwuIvN62UAguQfnNVzbge2uWLhA7TCXOn83DT0ZvyJmBI943MItgTovUob36SQ==
|
54 |
+
|
55 |
+
"@esbuild/[email protected]":
|
56 |
+
version "0.21.3"
|
57 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.3.tgz#da713eb80ff6c011ed01aa4deebb5fc758906046"
|
58 |
+
integrity sha512-HjCWhH7K96Na+66TacDLJmOI9R8iDWDDiqe17C7znGvvE4sW1ECt9ly0AJ3dJH62jHyVqW9xpxZEU1jKdt+29A==
|
59 |
+
|
60 |
+
"@esbuild/[email protected]":
|
61 |
+
version "0.21.3"
|
62 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.3.tgz#a7c5dc9e961009018d23ec53a43baa8c03c5a1d5"
|
63 |
+
integrity sha512-BGpimEccmHBZRcAhdlRIxMp7x9PyJxUtj7apL2IuoG9VxvU/l/v1z015nFs7Si7tXUwEsvjc1rOJdZCn4QTU+Q==
|
64 |
+
|
65 |
+
"@esbuild/[email protected]":
|
66 |
+
version "0.21.3"
|
67 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.3.tgz#b97543f3d8655365729f3702ed07f6e41be5e48e"
|
68 |
+
integrity sha512-5rMOWkp7FQGtAH3QJddP4w3s47iT20hwftqdm7b+loe95o8JU8ro3qZbhgMRy0VuFU0DizymF1pBKkn3YHWtsw==
|
69 |
+
|
70 |
+
"@esbuild/[email protected]":
|
71 |
+
version "0.21.3"
|
72 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.3.tgz#23b9064d5bc0bf28a115a2f9cf69f3b01cdfe01c"
|
73 |
+
integrity sha512-h0zj1ldel89V5sjPLo5H1SyMzp4VrgN1tPkN29TmjvO1/r0MuMRwJxL8QY05SmfsZRs6TF0c/IDH3u7XYYmbAg==
|
74 |
+
|
75 |
+
"@esbuild/[email protected]":
|
76 |
+
version "0.21.3"
|
77 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.3.tgz#4f2536327f6d444c0573bd35bbd3a67897dbd5da"
|
78 |
+
integrity sha512-dkAKcTsTJ+CRX6bnO17qDJbLoW37npd5gSNtSzjYQr0svghLJYGYB0NF1SNcU1vDcjXLYS5pO4qOW4YbFama4A==
|
79 |
+
|
80 |
+
"@esbuild/[email protected]":
|
81 |
+
version "0.21.3"
|
82 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.3.tgz#05e6f3a12a0dcd60672f25e8789a83cd3affa487"
|
83 |
+
integrity sha512-vnD1YUkovEdnZWEuMmy2X2JmzsHQqPpZElXx6dxENcIwTu+Cu5ERax6+Ke1QsE814Zf3c6rxCfwQdCTQ7tPuXA==
|
84 |
+
|
85 |
+
"@esbuild/[email protected]":
|
86 |
+
version "0.21.3"
|
87 |
+
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.3.tgz#994d347e7f530c33628e35e48ccde8f299adbcb6"
|
88 |
+
integrity sha512-IOXOIm9WaK7plL2gMhsWJd+l2bfrhfilv0uPTptoRoSb2p09RghhQQp9YY6ZJhk/kqmeRt6siRdMSLLwzuT0KQ==
|
89 |
+
|
90 |
+
"@esbuild/[email protected]":
|
91 |
+
version "0.21.3"
|
92 |
+
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.3.tgz#309d8c323632e9c70ee92cf5414fa65b5eb7e00e"
|
93 |
+
integrity sha512-uTgCwsvQ5+vCQnqM//EfDSuomo2LhdWhFPS8VL8xKf+PKTCrcT/2kPPoWMTs22aB63MLdGMJiE3f1PHvCDmUOw==
|
94 |
+
|
95 |
+
"@esbuild/[email protected]":
|
96 |
+
version "0.21.3"
|
97 |
+
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.3.tgz#28820f9431fe00f2b04aac57511754213ff060eb"
|
98 |
+
integrity sha512-vNAkR17Ub2MgEud2Wag/OE4HTSI6zlb291UYzHez/psiKarp0J8PKGDnAhMBcHFoOHMXHfExzmjMojJNbAStrQ==
|
99 |
+
|
100 |
+
"@esbuild/[email protected]":
|
101 |
+
version "0.21.3"
|
102 |
+
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.3.tgz#a1f7f98b85bd221fe0f545d01abc0e6123ae60dc"
|
103 |
+
integrity sha512-W8H9jlGiSBomkgmouaRoTXo49j4w4Kfbl6I1bIdO/vT0+0u4f20ko3ELzV3hPI6XV6JNBVX+8BC+ajHkvffIJA==
|
104 |
+
|
105 |
+
"@esbuild/[email protected]":
|
106 |
+
version "0.21.3"
|
107 |
+
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.3.tgz#c6c3c0b1a1dfc6327ef4db6aa4fb6efd9df531f7"
|
108 |
+
integrity sha512-EjEomwyLSCg8Ag3LDILIqYCZAq/y3diJ04PnqGRgq8/4O3VNlXyMd54j/saShaN4h5o5mivOjAzmU6C3X4v0xw==
|
109 |
+
|
110 |
+
"@esbuild/[email protected]":
|
111 |
+
version "0.21.3"
|
112 |
+
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.3.tgz#471b8d2cad1bd6479eee5acf04bba2c0e4d37e24"
|
113 |
+
integrity sha512-WGiE/GgbsEwR33++5rzjiYsKyHywE8QSZPF7Rfx9EBfK3Qn3xyR6IjyCr5Uk38Kg8fG4/2phN7sXp4NPWd3fcw==
|
114 |
+
|
115 |
+
"@esbuild/[email protected]":
|
116 |
+
version "0.21.3"
|
117 |
+
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.3.tgz#899c03576c4c28c83228f0e64dfa10edae99c9a2"
|
118 |
+
integrity sha512-xRxC0jaJWDLYvcUvjQmHCJSfMrgmUuvsoXgDeU/wTorQ1ngDdUBuFtgY3W1Pc5sprGAvZBtWdJX7RPg/iZZUqA==
|
119 |
+
|
120 |
+
"@types/[email protected]":
|
121 |
+
version "0.0.14"
|
122 |
+
resolved "https://registry.yarnpkg.com/@types/bootstrap-datepicker/-/bootstrap-datepicker-0.0.14.tgz#89bc5816dd0b802bf8d4923cf5461e2c98cbb450"
|
123 |
+
integrity sha512-ZBwEUuwYpehEHWtkaIDl55N3F6dPz6xEsoPW5esCZdtuhuZdNC/ebFV2n48kATcY5S2HD3V0O5/J8nuEgVk9fQ==
|
124 |
+
dependencies:
|
125 |
+
"@types/jquery" "*"
|
126 |
+
|
127 |
+
"@types/[email protected]":
|
128 |
+
version "3.4.0"
|
129 |
+
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-3.4.0.tgz#7e74a2421b3984ce53ab459e979a99a78dd802c6"
|
130 |
+
integrity sha512-LS05hVAAsX86qbHg7W+ydwBlNHrVCoFw6wEP3/uW4eYmRXl08bWmPeN/+onM+8qZTFfDgUlG/OItJI8SW972oQ==
|
131 |
+
dependencies:
|
132 |
+
"@types/jquery" "*"
|
133 |
+
|
134 |
+
"@types/datatables.net@^1.10.19":
|
135 |
+
version "1.12.0"
|
136 |
+
resolved "https://registry.yarnpkg.com/@types/datatables.net/-/datatables.net-1.12.0.tgz#cc0b0eb875997515aa10a59969ecb5fb03423ea4"
|
137 |
+
integrity sha512-HjvusoHf/XKdNI/pDP/qVDclUVOKF/PI4R/AlVeUdz3Z00+A4Dyu9N5hIXGk3cK36sy1A072UffYZ+1wwW0zWg==
|
138 |
+
dependencies:
|
139 |
+
datatables.net "*"
|
140 |
+
|
141 |
+
"@types/[email protected]":
|
142 |
+
version "2.3.0"
|
143 |
+
resolved "https://registry.yarnpkg.com/@types/ion-rangeslider/-/ion-rangeslider-2.3.0.tgz#dd089921c21acebc266c37e95d191e721ea21750"
|
144 |
+
integrity sha512-u1k2Ekfg7E/ByCrtqtMk98g3QMXnMyIjCKU397YfjhWnTrms2BteoTanMqe0MJ1zyBBiP6+bwbc7P4cU+RiL+w==
|
145 |
+
|
146 |
+
"@types/jquery@*", "@types/jquery@^3.5.30":
|
147 |
+
version "3.5.30"
|
148 |
+
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.30.tgz#888d584cbf844d3df56834b69925085038fd80f7"
|
149 |
+
integrity sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==
|
150 |
+
dependencies:
|
151 |
+
"@types/sizzle" "*"
|
152 |
+
|
153 |
+
"@types/jquery@patch:@types/[email protected]#./srcts/patch/types-jquery.patch":
|
154 |
+
version "3.5.5"
|
155 |
+
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903"
|
156 |
+
integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w==
|
157 |
+
dependencies:
|
158 |
+
"@types/sizzle" "*"
|
159 |
+
|
160 |
+
"@types/rstudio-shiny@https://github.com/rstudio/shiny#v1.7.0":
|
161 |
+
version "1.7.0"
|
162 |
+
resolved "https://github.com/rstudio/shiny#0fc861afb4c748b544ce1ac39be2e44563a7acf2"
|
163 |
+
dependencies:
|
164 |
+
"@types/bootstrap" "3.4.0"
|
165 |
+
"@types/bootstrap-datepicker" "0.0.14"
|
166 |
+
"@types/datatables.net" "^1.10.19"
|
167 |
+
"@types/ion-rangeslider" "2.3.0"
|
168 |
+
"@types/jquery" "patch:@types/[email protected]#./srcts/patch/types-jquery.patch"
|
169 |
+
"@types/selectize" "0.12.34"
|
170 |
+
|
171 |
+
"@types/[email protected]":
|
172 |
+
version "0.12.34"
|
173 |
+
resolved "https://registry.yarnpkg.com/@types/selectize/-/selectize-0.12.34.tgz#24a79e3176103019d2371ae9fc8487dcd1336cea"
|
174 |
+
integrity sha512-OAXHsgsHBMJBHnB6QrFlHVa0+gCE5lgjdpwJDlJLRhrcsM4I2VXrm2vsG00//Rpdm4XgnX17vrBpjHlWOk1ALw==
|
175 |
+
|
176 |
+
"@types/sizzle@*":
|
177 |
+
version "2.3.8"
|
178 |
+
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627"
|
179 |
+
integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==
|
180 |
+
|
181 |
+
datatables.net@*:
|
182 |
+
version "2.0.7"
|
183 |
+
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-2.0.7.tgz#8af61ac17334c37ae3bd77fc2828a3571e052041"
|
184 |
+
integrity sha512-cyW+HZwkMzb4bLrao/SS9/i64ZHiw5nYhXl+OwuOPgddG+R9O11iOEke8wYsdiyOQmjv2mE6xkEmRMZwHZc8zw==
|
185 |
+
dependencies:
|
186 |
+
jquery ">=1.7"
|
187 |
+
|
188 |
+
esbuild@^0.21.3:
|
189 |
+
version "0.21.3"
|
190 |
+
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.3.tgz#cbb10b100c768b0cfb35d61d9e70324553437c38"
|
191 |
+
integrity sha512-Kgq0/ZsAPzKrbOjCQcjoSmPoWhlcVnGAUo7jvaLHoxW1Drto0KGkR1xBNg2Cp43b9ImvxmPEJZ9xkfcnqPsfBw==
|
192 |
+
optionalDependencies:
|
193 |
+
"@esbuild/aix-ppc64" "0.21.3"
|
194 |
+
"@esbuild/android-arm" "0.21.3"
|
195 |
+
"@esbuild/android-arm64" "0.21.3"
|
196 |
+
"@esbuild/android-x64" "0.21.3"
|
197 |
+
"@esbuild/darwin-arm64" "0.21.3"
|
198 |
+
"@esbuild/darwin-x64" "0.21.3"
|
199 |
+
"@esbuild/freebsd-arm64" "0.21.3"
|
200 |
+
"@esbuild/freebsd-x64" "0.21.3"
|
201 |
+
"@esbuild/linux-arm" "0.21.3"
|
202 |
+
"@esbuild/linux-arm64" "0.21.3"
|
203 |
+
"@esbuild/linux-ia32" "0.21.3"
|
204 |
+
"@esbuild/linux-loong64" "0.21.3"
|
205 |
+
"@esbuild/linux-mips64el" "0.21.3"
|
206 |
+
"@esbuild/linux-ppc64" "0.21.3"
|
207 |
+
"@esbuild/linux-riscv64" "0.21.3"
|
208 |
+
"@esbuild/linux-s390x" "0.21.3"
|
209 |
+
"@esbuild/linux-x64" "0.21.3"
|
210 |
+
"@esbuild/netbsd-x64" "0.21.3"
|
211 |
+
"@esbuild/openbsd-x64" "0.21.3"
|
212 |
+
"@esbuild/sunos-x64" "0.21.3"
|
213 |
+
"@esbuild/win32-arm64" "0.21.3"
|
214 |
+
"@esbuild/win32-ia32" "0.21.3"
|
215 |
+
"@esbuild/win32-x64" "0.21.3"
|
216 |
+
|
217 |
+
jquery@>=1.7:
|
218 |
+
version "3.7.1"
|
219 |
+
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
|
220 |
+
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==
|
221 |
+
|
222 |
+
typescript@^5.4.5:
|
223 |
+
version "5.4.5"
|
224 |
+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
225 |
+
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
www/Adelie.png
DELETED
Binary file (79.1 kB)
|
|
www/Chinstrap.png
DELETED
Binary file (82 kB)
|
|
www/Gentoo.png
DELETED
Binary file (85.1 kB)
|
|
www/penguins.png
DELETED
Binary file (279 kB)
|
|