Akhil Iyer commited on
Commit
4e3e3d4
·
1 Parent(s): 92a6d05

Added README

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. Dockerfile +45 -0
  3. README.md +91 -5
  4. app.py +682 -0
  5. ffmpeg +3 -0
  6. requirements.txt +19 -0
  7. temporary_uploads/00.mp4 +3 -0
  8. temporary_uploads/01.mp4 +3 -0
  9. temporary_uploads/02.mp4 +3 -0
  10. temporary_uploads/03.mp4 +3 -0
  11. temporary_uploads/04.mp4 +3 -0
  12. temporary_uploads/05.mp4 +3 -0
  13. temporary_uploads/06.mp4 +3 -0
  14. temporary_uploads/07.mp4 +3 -0
  15. temporary_uploads/08.mp4 +3 -0
  16. temporary_uploads/09.mp4 +3 -0
  17. temporary_uploads/10.mp4 +3 -0
  18. temporary_uploads/11.mp4 +3 -0
  19. temporary_uploads/12.mp4 +3 -0
  20. temporary_uploads/13.mp4 +3 -0
  21. temporary_uploads/14.mp4 +3 -0
  22. temporary_uploads/15.mp4 +3 -0
  23. temporary_uploads/16.mp4 +3 -0
  24. temporary_uploads/17.mp4 +3 -0
  25. temporary_uploads/18.mp4 +3 -0
  26. temporary_uploads/19.mp4 +3 -0
  27. temporary_uploads/20.mp4 +3 -0
  28. temporary_uploads/21.mp4 +3 -0
  29. temporary_uploads/22.mp4 +3 -0
  30. temporary_uploads/23.mp4 +3 -0
  31. temporary_uploads/24.mp4 +3 -0
  32. temporary_uploads/25.mp4 +3 -0
  33. temporary_uploads/26.mp4 +3 -0
  34. temporary_uploads/27.mp4 +3 -0
  35. temporary_uploads/28.mp4 +3 -0
  36. temporary_uploads/29.mp4 +3 -0
  37. temporary_uploads/30.mp4 +3 -0
  38. temporary_uploads/31.mp4 +3 -0
  39. temporary_uploads/32.mp4 +3 -0
  40. temporary_uploads/33.mp4 +3 -0
  41. temporary_uploads/34.mp4 +3 -0
  42. temporary_uploads/35.mp4 +3 -0
  43. temporary_uploads/36.mp4 +3 -0
  44. temporary_uploads/37.mp4 +3 -0
  45. temporary_uploads/38.mp4 +3 -0
  46. temporary_uploads/39.mp4 +3 -0
  47. temporary_uploads/40.mp4 +3 -0
  48. temporary_uploads/41.mp4 +3 -0
  49. temporary_uploads/42.mp4 +3 -0
  50. temporary_uploads/43.mp4 +3 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ffmpeg filter=lfs diff=lfs merge=lfs -text
37
+ temporary_uploads/** filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04
2
+ LABEL maintainer="Hugging Face"
3
+
4
+ ARG DEBIAN_FRONTEND=noninteractive
5
+
6
+ RUN useradd -m -u 1000 user
7
+
8
+ # Switch to the "user" user
9
+ USER user
10
+
11
+ # Set home to the user's home directory
12
+ ENV HOME=/home/user \
13
+ PATH=/home/user/.local/bin:$PATH
14
+
15
+ WORKDIR /code
16
+
17
+ COPY --chown=user ./requirements.txt /code/requirements.txt
18
+
19
+ USER root
20
+
21
+ RUN apt update
22
+ RUN apt install -y git libsndfile1-dev tesseract-ocr espeak-ng python3 python3-pip ffmpeg
23
+ RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
24
+
25
+ ARG PYTORCH='2.0.1'
26
+ ARG TORCH_VISION=''
27
+ ARG TORCH_AUDIO=''
28
+ # Example: `cu102`, `cu113`, etc.
29
+ ARG CUDA='cu118'
30
+
31
+ RUN python3 -m pip install --no-cache-dir --upgrade pip
32
+ RUN [ ${#PYTORCH} -gt 0 ] && VERSION='torch=='$PYTORCH'.*' || VERSION='torch'; python3 -m pip install --no-cache-dir -U $VERSION --extra-index-url https://download.pytorch.org/whl/$CUDA
33
+ RUN [ ${#TORCH_VISION} -gt 0 ] && VERSION='torchvision=='TORCH_VISION'.*' || VERSION='torchvision'; python3 -m pip install --no-cache-dir -U $VERSION --extra-index-url https://download.pytorch.org/whl/$CUDA
34
+ RUN [ ${#TORCH_AUDIO} -gt 0 ] && VERSION='torchaudio=='TORCH_AUDIO'.*' || VERSION='torchaudio'; python3 -m pip install --no-cache-dir -U $VERSION --extra-index-url https://download.pytorch.org/whl/$CUDA
35
+
36
+ RUN python3 -m pip install --no-cache-dir --upgrade -r /code/requirements.txt
37
+
38
+ ENV OPENAI_API_KEY="<PASTE YOUR OPENAI KEY HERE>"
39
+ ENV GOOGLE_APPLICATION_CREDENTIALS="<PASTE THE PATH TO YOUR GOOGLE CREDENTIALS JSON FILE HERE>"
40
+
41
+ USER user
42
+
43
+ COPY --chown=user . .
44
+
45
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,96 @@
1
  ---
2
- title: ShortScribe Pipeline
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Short Video Descriptions
3
+ emoji: 🌖
4
+ colorFrom: red
5
+ colorTo: red
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # ShortScribe Pipeline
11
+
12
+ This repository provides code for the paper [Making Short-Form Videos Accessible With Hierarchical Video Summaries](https://arxiv.org/abs/2402.10382). In this repository, we introduce the pipeline for ShortScribe, a tool that makes short-form videos accessible to blind and low-vision users by generating summaries at varying levels of depth. This source code specifically provides an API for generating the summaries. To see the source code for ShortScribe's interface, see this GitHub repository [here](https://github.com/tessvandaele/tiktok-simulation)
13
+
14
+ # Installing ShortScribe
15
+
16
+ Before building the environment to run the ShortScribe pipeline, ensure that you have enough resources and credentials. You will need Google Cloud service account credentials in the form of a JSON file and an OpenAI API Key. We deployed our system on an NVIDIA A100 GPU with about 50 GB memory using PyTorch 2.0.1+cu118.
17
+
18
+ To get a local copy of the pipeline, run ```git lfs install``` and then ```git clone git@hf.co:spaces/akhil03/ShortScribe-Pipeline```
19
+
20
+ # Running ShortScribe
21
+
22
+ The Dockerfile builds the environment with the necessary packages and then runs a development server to send API requests to. To setup the Docker container, perform the following steps:
23
+
24
+ ```
25
+ docker build -t <image-name>
26
+ docker run -p 80:80 <image-name>
27
+ docker exec -it <container-name>
28
+ ```
29
+
30
+ If you are pushing this Dockerfile to your own Hugging Face repository, the Docker container will automatically build and execute, so you can ignore the above commands if this is the case.
31
+
32
+ # API Calls
33
+
34
+ The API Calls are designed such that the video to be summarized must first be uploaded to the `temporary_uploads/` folder before trying to send an API request. Please make sure that the video is titled as `<number>.mp4` (e.g. `01.mp4`) and uploaded to `temporary_uploads/` before making any API calls.
35
+
36
+ ### getVideoData/<video-id>
37
+
38
+ Given the video ID (specified in the file name), returns all of the data extracted from the video before being summarized by GPT-4. Returns a JSON object shown as follows:
39
+
40
+ ```json
41
+ {
42
+ "start": "Sample text", // Summary of the extracted data of the entire video without a word limit (float)
43
+ "end": "Sample text", // The end of the shot in terms of seconds (float)
44
+ "text_on_screen": "Sample text", // On-screen text in the shot (string)
45
+ "transcript_text": "Sample text", // Audio transcript of the shot (string)
46
+ "image_captions": ["Sample text", "Sample text", "Sample text", "Sample text", "Sample text"], // Five candidate image captions generated by BLIP-2 (not sorted in any particular order)
47
+ "image_captions_clip": [
48
+ {
49
+ "text": "Sample text", // Image caption generated by BLIP-2
50
+ "score": 1.0, // Image caption similarity score generated by CLIP
51
+ },
52
+ ... 4 more ...
53
+ ]
54
+ }
55
+ ```
56
+
57
+ ### getShotSummaries/<video-id>
58
+
59
+ Given the video ID (specified by the file name), returns a list of JSON objects for each shot of the video. The format for each JSON object in the list is shown below:
60
+
61
+ ```json
62
+ {
63
+ "start": 0.0, // The start of the shot in terms of seconds (float)
64
+ "end": 5.75, // The end of the shot in terms of seconds (float)
65
+ "text_on_screen": "Sample text", // On-screen text in the shot (string)
66
+ "per_shot_summaries": "Summary of the shot generated by GPT-4" // Summary of the shot (string)
67
+ }
68
+ ```
69
+
70
+ ### getVideoSummary/<video-id>
71
+
72
+ Given the video ID (specified in the file name), returns all of the overall summaries of the video (short description, long description, 25-word description, 50-word description) generated by GPT-4. Returns a JSON object shown as follows:
73
+
74
+ ```json
75
+ {
76
+ "video_description": "Sample text", // Summary of the extracted data of the entire video without a word limit
77
+ "summary_10": "Sample text", // Summary of the extracted data of the entire video in 10 words
78
+ "summary_25": "Sample text", // Summary of the extracted data of the entire video in 25 words
79
+ "summary_50": "Sample text" // Summary of the extracted data of the entire video in 50 words
80
+ }
81
+ ```
82
+
83
+ # Credits and Citation
84
+
85
+ If you have any questions or issues related to the source code, feel free to reach out to Akhil Iyer (akhil.iyer@utexas.edu)
86
+
87
+ If our work is useful to you, please cite our work with the following citation:
88
+
89
+ ```
90
+ @article{van2024making,
91
+ title={Making Short-Form Videos Accessible with Hierarchical Video Summaries},
92
+ author={Van Daele, Tess and Iyer, Akhil and Zhang, Yuning and Derry, Jalyn C and Huh, Mina and Pavel, Amy},
93
+ journal={arXiv preprint arXiv:2402.10382},
94
+ year={2024}
95
+ }
96
+ ```
app.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ from google.cloud import videointelligence, speech, storage
4
+ import io
5
+ import json
6
+ import cv2
7
+ import torch
8
+ import clip
9
+ from PIL import Image
10
+ from transformers import Blip2Processor, Blip2ForConditionalGeneration
11
+ import openai
12
+ import wave
13
+ from fastapi import FastAPI, File, UploadFile
14
+ from fastapi.responses import JSONResponse
15
+ import uvicorn
16
+ from pydantic import BaseModel
17
+
18
+ clip_loaded, blip_loaded = False, False
19
+ cred_file = "<PASTE THE PATH TO YOUR GOOGLE CREDENTIALS JSON FILE HERE>"
20
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = cred_file
21
+ os.environ["OPENAI_API_KEY"] = "<PASTE YOUR OPENAI KEY HERE>"
22
+ openai_api_key = "<PASTE YOUR OPENAI KEY HERE>"
23
+
24
+
25
+ def get_timestamps(video):
26
+
27
+ tiktok_vid = video
28
+
29
+ ffmpeg_command = """ffmpeg -i tiktokvideo -filter:v "select='gt(scene,0.2)',showinfo" -f null - 2> ffout"""
30
+ ffmpeg_command = ffmpeg_command.replace("tiktokvideo", tiktok_vid)
31
+
32
+ grep_command = """grep showinfo ffout | grep 'pts_time:[0-9.]*' -o | grep '[0-9]*\.[0-9]*' -o > timestamps.txt"""
33
+
34
+ os.system(ffmpeg_command)
35
+ os.system(grep_command)
36
+
37
+ with open('timestamps.txt', "r") as t:
38
+ times = [0] + [float(k) for k in t.read().split("\n") if k]
39
+
40
+ times_output = "Times: "
41
+ print(times)
42
+ for time in times:
43
+ times_output += str(time) + ", "
44
+
45
+ return times_output
46
+
47
+ def get_text_annotations(video, cred_file):
48
+ os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = cred_file
49
+
50
+ # get text annotation results
51
+ # OCR
52
+ video_client = videointelligence.VideoIntelligenceServiceClient()
53
+ features = [videointelligence.Feature.TEXT_DETECTION]
54
+ video_context = videointelligence.VideoContext()
55
+
56
+ with io.open(video, "rb") as file:
57
+ input_content = file.read()
58
+
59
+ operation = video_client.annotate_video(
60
+ request={
61
+ "features": features,
62
+ "input_content": input_content,
63
+ "video_context": video_context,
64
+ }
65
+ )
66
+
67
+ print("\nProcessing video for text detection.")
68
+ result = operation.result(timeout=300)
69
+
70
+ # The first result is retrieved because a single video was processed.
71
+ annotation_result = result.annotation_results[0]
72
+
73
+ # format text annotation results
74
+ # for each video-detected segment, get confidence
75
+ text_annotation_json = []
76
+
77
+ for text_annotation in annotation_result.text_annotations:
78
+
79
+ text_segment = text_annotation.segments[0]
80
+ start_time = text_segment.segment.start_time_offset
81
+ end_time = text_segment.segment.end_time_offset
82
+
83
+ frame = text_segment.frames[0]
84
+ time_offset = frame.time_offset
85
+
86
+ current_text_annotation_json = {
87
+ "text": text_annotation.text,
88
+ "start": start_time.seconds + start_time.microseconds * 1e-6,
89
+ "end": end_time.seconds + end_time.microseconds * 1e-6,
90
+ "confidence": text_segment.confidence,
91
+ "vertecies": []
92
+ }
93
+
94
+ for vertex in frame.rotated_bounding_box.vertices:
95
+ current_text_annotation_json["vertecies"].append([vertex.x, vertex.y])
96
+ text_annotation_json.append(current_text_annotation_json)
97
+
98
+ out = []
99
+
100
+ for text_annotation in annotation_result.text_annotations:
101
+
102
+ text_segment = text_annotation.segments[0]
103
+ start_time = text_segment.segment.start_time_offset
104
+ end_time = text_segment.segment.end_time_offset
105
+
106
+ start_time_s = start_time.seconds + start_time.microseconds * 1e-6
107
+ end_time_s = end_time.seconds + end_time.microseconds * 1e-6
108
+ confidence = text_segment.confidence
109
+
110
+ frame = text_segment.frames[0]
111
+ top_left = frame.rotated_bounding_box.vertices[0]
112
+
113
+ out.append([start_time_s, end_time_s, text_annotation.text, confidence, top_left.y])
114
+
115
+ simple_text = [k for k in sorted(out, key= lambda k: k[0] + k[4]) if k[3] > 0.95]
116
+
117
+ for s in simple_text:
118
+ print(s)
119
+
120
+ with open('annotation.json', 'w') as f:
121
+ json.dump(text_annotation_json, f, indent=4)
122
+
123
+ with open('simple_annotation.json', 'w') as f:
124
+ json.dump(simple_text, f, indent=4)
125
+
126
+ def transcribe_video(video, cred_file):
127
+
128
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = cred_file
129
+
130
+ if os.path.exists("output_audio.wav"):
131
+ os.remove("output_audio.wav")
132
+ else:
133
+ print("NOT THERE")
134
+
135
+ wav_cmd = f"ffmpeg -i {video} output_audio.wav"
136
+ os.system(wav_cmd)
137
+
138
+ print(os.path.exists("output_audio.wav"))
139
+
140
+ gcs_uri = upload_file_to_bucket("output_audio.wav", cred_file)
141
+
142
+ speech_client = speech.SpeechClient()
143
+
144
+ with open("output_audio.wav", "rb") as f:
145
+ audio_content = f.read()
146
+
147
+
148
+ audio = speech.RecognitionAudio(uri=gcs_uri)
149
+ sample_rate_hertz, audio_channel_count = wav_data("output_audio.wav")
150
+
151
+ config = speech.RecognitionConfig(
152
+ encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
153
+ sample_rate_hertz=sample_rate_hertz,
154
+ audio_channel_count=audio_channel_count,
155
+ language_code="en-US",
156
+ model="video",
157
+ enable_word_time_offsets=True,
158
+ enable_automatic_punctuation=True,
159
+ enable_word_confidence=True
160
+ )
161
+
162
+ request = speech.LongRunningRecognizeRequest(
163
+ config=config,
164
+ audio=audio
165
+ )
166
+
167
+ operation = speech_client.long_running_recognize(request=request)
168
+
169
+ print("Waiting for operation to complete...")
170
+
171
+ response = operation.result(timeout=600)
172
+
173
+ out = []
174
+ for i, result in enumerate(response.results):
175
+ alternative = result.alternatives[0]
176
+
177
+ if len(alternative.words) > 0:
178
+ alt_start = alternative.words[0].start_time.seconds + alternative.words[0].start_time.microseconds * 1e-6
179
+ alt_end = alternative.words[-1].end_time.seconds + alternative.words[-1].end_time.microseconds * 1e-6
180
+
181
+ for word in alternative.words:
182
+ out.append([word.word,
183
+ word.start_time.seconds + word.start_time.microseconds * 1e-6,
184
+ word.end_time.seconds + word.end_time.microseconds * 1e-6,
185
+ word.confidence])
186
+
187
+ simple_text = [k for k in sorted(out, key= lambda k: k[1])]
188
+ for s in simple_text:
189
+ print(s)
190
+
191
+ with open("speech_transcriptions.json", "w") as f:
192
+ json.dump(simple_text, f, indent=4)
193
+
194
+ return simple_text
195
+
196
+ def wav_data(wav_file):
197
+
198
+ with wave.open(wav_file, 'rb') as wf:
199
+ sample_rate_hertz = wf.getframerate()
200
+ audio_channel_count = wf.getnchannels()
201
+
202
+ return sample_rate_hertz, audio_channel_count
203
+
204
+ def get_shot_frames(video, shot_text):
205
+ cam = cv2.VideoCapture(video)
206
+ fps = cam.get(cv2.CAP_PROP_FPS)
207
+ frame_count = int(cam.get(cv2.CAP_PROP_FRAME_COUNT))
208
+ duration = frame_count/fps
209
+
210
+ with open('timestamps.txt', 'r') as t:
211
+ times = [0] + [float(k) for k in t.read().split('\n') if k]
212
+ print("Times: ", times)
213
+
214
+ with open('simple_annotation.json', 'r') as f:
215
+ simple_text = json.load(f)
216
+
217
+ with open('speech_transcriptions.json', 'r') as f:
218
+ transcriptions = json.load(f)
219
+
220
+ for i, time in enumerate(times):
221
+ current_time = time
222
+ next_time = times[i + 1] if i < len(times) - 1 else duration
223
+
224
+ rel_text = [s for s in simple_text if s[0] >= current_time and s[0] < next_time]
225
+ plain_rel_text = ' '.join([s[2] for s in rel_text])
226
+
227
+ rel_transcriptions = [t for t in transcriptions if t[1] >= current_time and t[1] < next_time]
228
+ plain_transcriptions = ' '.join([t[0] for t in rel_transcriptions])
229
+
230
+ shot_text.append({
231
+ "start": current_time,
232
+ "end": next_time,
233
+ "text_on_screen": plain_rel_text,
234
+ "transcript_text": plain_transcriptions
235
+ })
236
+
237
+ frames = []
238
+ for i, shot in enumerate(shot_text):
239
+ keyframe_time = (shot["end"] - shot["start"])/2 + shot["start"]
240
+ cam.set(1, int(fps * (keyframe_time)))
241
+ ret, frame = cam.read()
242
+
243
+ if ret:
244
+ cv2.imwrite('shot' + str(i) + '.png', frame)
245
+ frame_copy = Image.fromarray(frame).convert('RGB')
246
+ frames.append(frame_copy)
247
+
248
+ return frames
249
+
250
+
251
+ def load_clip_model():
252
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
253
+ clip_model, preprocess = clip.load('ViT-B/32', device=device)
254
+
255
+ return clip_model, preprocess, device
256
+
257
+ def clip_score(fn, text_list, clip_model, preprocess, clip_device):
258
+ fn.show()
259
+ image = preprocess(fn).unsqueeze(0).to(clip_device)
260
+ text = clip.tokenize(text_list).to(clip_device)
261
+
262
+ with torch.no_grad():
263
+ image_features = clip_model.encode_image(image)
264
+ text_features = clip_model.encode_text(text)
265
+
266
+ logits_per_image, logits_per_text = clip_model(image, text)
267
+ probs = logits_per_image.softmax(dim=-1).cpu().numpy()
268
+
269
+ return probs
270
+
271
+
272
+ def load_blip_model():
273
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
274
+
275
+ processor = Blip2Processor.from_pretrained('Salesforce/blip2-flan-t5-xxl')
276
+ model = Blip2ForConditionalGeneration.from_pretrained(
277
+ 'Salesforce/blip2-flan-t5-xxl', torch_dtype=torch.float16
278
+ )
279
+
280
+ model = model.to(device)
281
+
282
+ return model, processor, device
283
+
284
+ def run_blip(shot_text, frames, model, processor, device, clip_model, preprocess, clip_device):
285
+ # get a caption for each image
286
+
287
+ for i, shot in enumerate(shot_text):
288
+ if not os.path.exists(f"shot{i}.png"):
289
+ shot_text[i]["image_captions"] = ["" for _ in range(5)]
290
+ shot_text[i]["image_captions_clip"] = [{"text": "", "score": 0.0} for _ in range(5)]
291
+ continue
292
+
293
+ image = Image.open(f"shot{i}.png").convert('RGB')
294
+
295
+ with torch.no_grad():
296
+ # nucleus sampling
297
+ gen_texts = []
298
+ for j in range(5):
299
+ inputs = processor(images=image, return_tensors="pt").to(device, torch.float16)
300
+ generated_ids = model.generate(**inputs, min_length=5, max_length=20, do_sample=True, top_p=0.9)
301
+ generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
302
+ gen_texts.append(generated_text)
303
+
304
+ image.show()
305
+ shot_text[i]["image_captions"] = [gen_texts[j] for j in range(len(gen_texts))]
306
+ print(shot_text[i]["image_captions"])
307
+
308
+ clip_scores = clip_score(image.copy(), shot_text[i]["image_captions"], clip_model, preprocess, clip_device)[0]
309
+ print(clip_scores)
310
+ shot_text[i]["image_captions_clip"] = [{"text": shot_text[i]["image_captions"][j],
311
+ "score": float(clip_scores[j])} for j in range(len(shot_text[i]["image_captions"]))]
312
+
313
+ shot_text[i]["image_captions_clip"] = sorted(shot_text[i]["image_captions_clip"], key=lambda x: x["score"] * -1)
314
+
315
+ for s in shot_text[i]["image_captions_clip"]:
316
+ print(s)
317
+
318
+ shot_text[i]["image_captions"] = [t["text"] for t in shot_text[i]["image_captions_clip"] if "caption" not in t["text"]]
319
+
320
+ for i, shot in enumerate(shot_text):
321
+ if os.path.exists(f"shot{i}.png"):
322
+ os.remove(f"shot{i}.png")
323
+
324
+ return shot_text
325
+
326
+ def get_summaries(summary_input, openai_key):
327
+ gpt_system_prompt = f'''Your task is to generate a summary paragraph for an entire short-form video based on data extracted from the video. Your summary must be a holistic description of the full video. \n
328
+
329
+ The text in quotations defines the format of the data that I will provide you. The video data comprises of data extracted from all shots of the video.\n
330
+ The data is formatted in the structure defined in the quotations:\n
331
+ "\n
332
+ SHOT NUMBER
333
+ Duration: the number of seconds that the shot lasts
334
+ Text on screen: Any text that appears in the shot
335
+ Shot audio transcript: Any speech that is in the shot
336
+ Shot description: A short visual description of what is happening in the shot
337
+ "\n
338
+ '''
339
+
340
+ gpt_user_prompt = f'''Perform this video summarization task for the video below, where the data is delimited by triple quotations.\n
341
+ Video: \n"""{summary_input}"""\n '''
342
+
343
+ messages = [{"role": "system", "content": gpt_system_prompt},
344
+ {"role": "user", "content": gpt_user_prompt}]
345
+ responses = []
346
+
347
+ response = openai.ChatCompletion.create(
348
+ model='gpt-4',
349
+ messages=messages
350
+ )
351
+
352
+ messages.append(response.choices[0].message)
353
+ responses.append(response.choices[0].message["content"])
354
+
355
+ for word_limit in [50, 25, 10]:
356
+
357
+ condense_prompt = f'''Condense the summary below such that the response adheres to a {word_limit} word limit.\n
358
+ Summary: """ {response.choices[0].message["content"]} """\n'''
359
+
360
+ messages.append({"role": "user", "content": condense_prompt})
361
+
362
+ response = openai.ChatCompletion.create(
363
+ model='gpt-4',
364
+ messages=messages
365
+ )
366
+
367
+ messages.append(response.choices[0].message)
368
+ responses.append(response.choices[0].message["content"])
369
+
370
+ return responses
371
+
372
+ def get_shot_summaries(summary_input, openai_key):
373
+
374
+ gpt_system_prompt = f'''Your task is to generate a summary for each shot of a short-form video based on data extracted from the video.\n
375
+
376
+ The text in quotations defines the format of the data that I will provide you. The video data comprises of data extracted from all shots of the video.\n
377
+ The data is formatted in the structure defined in the quotations:\n
378
+ "\n
379
+ SHOT NUMBER
380
+ Duration: the number of seconds that the shot lasts
381
+ Text on screen: Any text that appears in the shot
382
+ Shot audio transcript: Any speech that is in the shot
383
+ Shot description: A short visual description of what is happening in the shot
384
+ "\n
385
+
386
+ All of the summaries you create must satisfy the following constraints:\n
387
+
388
+ 1. If the field for text on screen is empty, do not include references to text on screen in the summary.\n
389
+ 2. If the field for shot audio transcript is empty, do not include references to shot audio transcript in the summary.\n
390
+ 3. If the field for shot description is empty, do not include references to the shot description in the summary.\n
391
+ 4. If the field for shot description is empty, do not include references to shot description in the summary.\n
392
+ 5. Do not include references to Tiktok logos or Tiktok usernames in the summary.\n
393
+
394
+ There must be a summary for every shot in the data.
395
+
396
+ Provide the summaries in a newline-separated format. There must be exactly one summary for every shot.\n
397
+ You must strictly follow the format inside the quotations.\n
398
+
399
+ "Your first summary\n
400
+ Your second summary\n
401
+ Your third summary\n
402
+ More of your summaries...\n
403
+ Your last summary\n
404
+ "
405
+
406
+ '''
407
+
408
+ gpt_user_prompt = f'''Perform this summarization task for the video below, where the data is delimited by triple quotations.\n
409
+ Video: \n"""{summary_input}"""\n '''
410
+
411
+
412
+ messages = [{"role": "system", "content": gpt_system_prompt},
413
+ {"role": "user", "content": gpt_user_prompt}]
414
+ responses = []
415
+
416
+ response = openai.ChatCompletion.create(
417
+ model='gpt-4',
418
+ messages=messages
419
+ )
420
+
421
+ messages.append(response.choices[0].message)
422
+ responses.append(response.choices[0].message["content"])
423
+
424
+ responses[0] = responses[0].strip()
425
+ shot_summary_list = [shot_summ.strip().strip('[]') for shot_summ in responses[0].split("\n")
426
+ if shot_summ.strip().strip('[]') != "" and shot_summ.strip().strip('[]') != " "]
427
+
428
+ print(responses[0])
429
+ print()
430
+ print(shot_summary_list)
431
+ print()
432
+
433
+ return shot_summary_list
434
+
435
+ def upload_file_to_bucket(filename, cred_file):
436
+ storage_client = storage.Client.from_service_account_json(
437
+ cred_file,
438
+ project="short-video-descriptions")
439
+
440
+ bucket_name = "short-video-descriptions"
441
+ destination_blob_name = filename
442
+ bucket = storage_client.get_bucket(bucket_name)
443
+ blob = bucket.blob(destination_blob_name)
444
+
445
+ blob.upload_from_filename(filename)
446
+
447
+ return f"gs://{bucket_name}/{destination_blob_name}"
448
+
449
+
450
+ def blob_exists(filename, cred_file):
451
+ storage_client = storage.Client.from_service_account_json(
452
+ cred_file,
453
+ project="short-video-descriptions")
454
+
455
+ bucket_name = 'short-video-descriptions'
456
+ bucket = storage_client.bucket(bucket_name)
457
+ stats = storage.Blob(bucket=bucket, name=filename).exists(storage_client)
458
+
459
+ return stats
460
+
461
+ def del_blob(blob_name, cred_file):
462
+ storage_client = storage.Client.from_service_account_json(
463
+ cred_file,
464
+ project="short-video-descriptions")
465
+
466
+ bucket = storage_client.bucket("short-video-descriptions")
467
+ blob = bucket.blob(blob_name)
468
+ generation_match_precondition = None
469
+
470
+ # Optional: set a generation-match precondition to avoid potential race conditions
471
+ # and data corruptions. The request to delete is aborted if the object's
472
+ # generation number does not match your precondition.
473
+ blob.reload() # Fetch blob metadata to use in generation_match_precondition.
474
+ generation_match_precondition = blob.generation
475
+
476
+ blob.delete(if_generation_match=generation_match_precondition)
477
+
478
+ print(f"Blob {blob_name} deleted.")
479
+
480
+ def get_summary_input(shot_text):
481
+ summ_input = ""
482
+ for i, s in enumerate(shot_text):
483
+ summ_input += f"SHOT {i + 1}\n"
484
+ summ_input += f"Duration: {round(s['end'] - s['start'])} seconds\n"
485
+ summ_input += f"Text on screen: {s['text_on_screen']}\n"
486
+ summ_input += f"Shot audio transcript: {s['transcript_text']}\n"
487
+ summ_input += f"Shot description: {s['image_captions'][0] if len(s['image_captions']) > 0 else ''}\n"
488
+ summ_input += "\n"
489
+
490
+ return summ_input
491
+
492
+ def get_video_data(video, transcript, cred_file):
493
+ shot_text = []
494
+ timestamps_output = get_timestamps(video)
495
+ get_text_annotations(video, cred_file.name)
496
+ transcribe_video(video, cred_file.name)
497
+ frames = get_shot_frames(video, shot_text)
498
+ shot_text = run_blip(shot_text, frames, model, processor, device, clip_model, preprocess, clip_device)
499
+
500
+ return shot_text
501
+
502
+ def get_video_information(video, cred_file, openai_key):
503
+ shot_text = []
504
+ timestamps_output = get_timestamps(video)
505
+ get_text_annotations(video, cred_file.name)
506
+ transcribe_video(video, cred_file.name)
507
+ frames = get_shot_frames(video, shot_text)
508
+ shot_text = run_blip(shot_text, frames, model, processor, device,
509
+ clip_model, preprocess, clip_device)
510
+
511
+ print("FINAL INPUT")
512
+ print(shot_text)
513
+
514
+ with open('cur_shots.json', 'w') as f:
515
+ json.dump(shot_text, f, indent=4)
516
+
517
+ summary_input = get_summary_input(shot_text)
518
+ summaries = get_summaries(summary_input, openai_key)
519
+
520
+ print("ALL SUMMARIES")
521
+ for summary in summaries:
522
+ print(summary)
523
+
524
+ return (shot_text, summary_input) + (*summaries,)
525
+
526
+ def get_per_shot_information(video, cred_file, openai_key):
527
+ shot_text = []
528
+ timestamps_output = get_timestamps(video)
529
+ get_text_annotations(video, cred_file.name)
530
+ transcribe_video(video, cred_file.name)
531
+ frames = get_shot_frames(video, shot_text)
532
+ # vtt_content = transcribe_audio_google(video)
533
+ # get_audio_transcript("transcribed_captions.vtt", shot_text)
534
+ shot_text = run_blip(shot_text, frames, model, processor, device,
535
+ clip_model, preprocess, clip_device)
536
+
537
+ print("FINAL INPUT")
538
+ print(shot_text)
539
+
540
+ with open('cur_shots.json', 'w') as f:
541
+ json.dump(shot_text, f, indent=4)
542
+
543
+ summary_input = get_summary_input(shot_text)
544
+ per_shot_summaries = get_shot_summaries(summary_input, openai_key)
545
+ per_shot_data = create_per_shot_dict(shot_text, per_shot_summaries)
546
+
547
+ return (per_shot_data, per_shot_summaries, summary_input)
548
+
549
+ def create_per_shot_dict(shot_text, per_shot_summaries):
550
+
551
+ for elem in per_shot_summaries:
552
+ print(elem)
553
+
554
+ per_shot_data = []
555
+ for i, s in enumerate(shot_text):
556
+ cur_summ = ""
557
+ if i < len(per_shot_summaries):
558
+ cur_summ = per_shot_summaries[i]
559
+ per_shot_data.append({
560
+ "start": s["start"],
561
+ "end": s["end"],
562
+ "text_on_screen": s["text_on_screen"],
563
+ "per_shot_summaries": cur_summ
564
+ })
565
+
566
+ return per_shot_data
567
+
568
+ with gr.Blocks() as demo:
569
+ with gr.Row():
570
+ video = gr.Video(label='Video To Describe', interactive=True)
571
+
572
+ with gr.Column():
573
+ api_cred_file = gr.File(label='Google API Credentials File', file_types=['.json'])
574
+ openai_key = gr.Textbox(label="OpenAI API Key")
575
+
576
+
577
+ with gr.Row():
578
+ summary_btn = gr.Button("Summarize Full Video")
579
+ summary_per_shot_btn = gr.Button("Summarize Each Shot")
580
+
581
+ with gr.Row():
582
+ summary_input = gr.Textbox(label="Extracted Video Data")
583
+
584
+ with gr.Row():
585
+ summary = gr.Textbox(label='Summary')
586
+ with gr.Column():
587
+ summary_10 = gr.Textbox(label='10-word Summary')
588
+ summary_25 = gr.Textbox(label='25-word Summary')
589
+ summary_50 = gr.Textbox(label='50-word Summary')
590
+
591
+ with gr.Row():
592
+ per_shot_summaries = gr.Textbox(label="Per Shot Summaries")
593
+
594
+ with gr.Row():
595
+ shot_data = gr.JSON(label='Shot Data')
596
+
597
+ # inputs = [video, transcript, api_cred_file, openai_key]
598
+ inputs = [video, api_cred_file, openai_key]
599
+ outputs = [shot_data, summary_input, summary, summary_50, summary_25, summary_10]
600
+
601
+ summary_btn.click(fn=get_video_information, inputs=inputs, outputs=outputs)
602
+ summary_per_shot_btn.click(fn=get_per_shot_information, inputs=inputs, outputs=[shot_data, per_shot_summaries, summary_input])
603
+
604
+
605
+
606
+ def analyze_video(video_id: str):
607
+ shot_text = []
608
+
609
+ video_path = f"temporary_uploads/{video_id}.mp4"
610
+
611
+ timestamps_output = get_timestamps(video_path)
612
+ get_text_annotations(video_path, cred_file)
613
+ transcribe_video(video_path, cred_file)
614
+ frames = get_shot_frames(video_path, shot_text)
615
+ shot_text = run_blip(shot_text, frames, model, processor, device, clip_model, preprocess, clip_device)
616
+
617
+ return shot_text
618
+
619
+ def summarize_video(video_id: str):
620
+
621
+ video_path = f"temporary_uploads/{video_id}.mp4"
622
+ shot_text = analyze_video(video_id)
623
+ summary_input = get_summary_input(shot_text)
624
+ summaries = get_summaries(summary_input, openai_api_key)
625
+
626
+ summary_json = {
627
+ "video_description": summaries[0],
628
+ "summary_10": summaries[3],
629
+ "summary_25": summaries[2],
630
+ "summary_50": summaries[1]
631
+ }
632
+
633
+ return summary_json
634
+
635
+ def summarize_shots(video_id: str):
636
+
637
+ video_path = f"temporary_uploads/{video_id}.mp4"
638
+ shot_text = analyze_video(video_id)
639
+ summary_input = get_summary_input(shot_text)
640
+ per_shot_summaries = get_shot_summaries(summary_input, "")
641
+ per_shot_data = create_per_shot_dict(shot_text, per_shot_summaries)
642
+
643
+ return per_shot_data
644
+
645
+ app = FastAPI()
646
+ app = gr.mount_gradio_app(app, demo, path="/gradio")
647
+
648
+ @app.get("/")
649
+ async def read_main():
650
+ return {"message": "Welcome to ShortVideoA11y! Go to https://utcs-hci-short-video-descriptions.hf.space/gradio for an interactive demo!"}
651
+
652
+ @app.get("/getVideoData/{video_id}")
653
+ async def create_video_data(video_id: str):
654
+ try:
655
+ shot_text = analyze_video(video_id)
656
+ return JSONResponse(content=shot_text)
657
+
658
+ except Exception as e:
659
+ error_content = {"error": str(e)}
660
+ return JSONResponse(content=error_content, status_code=400)
661
+
662
+ @app.get("/getShotSummaries/{video_id}")
663
+ async def create_shot_summaries(video_id: str):
664
+
665
+ per_shot_data = summarize_shots(video_id)
666
+ return JSONResponse(content=per_shot_data)
667
+
668
+ @app.get("/getVideoSummary/{video_id}")
669
+ async def create_video_summaries(video_id: str):
670
+
671
+ vid_summaries = summarize_video(video_id)
672
+ return JSONResponse(content=vid_summaries)
673
+
674
+ demo.queue()
675
+
676
+ if not clip_loaded:
677
+ clip_model, preprocess, clip_device = load_clip_model()
678
+ clip_loaded = True
679
+
680
+ if not blip_loaded:
681
+ model, processor, device = load_blip_model()
682
+ blip_loaded = True
ffmpeg ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cfe20936c83ecf5d68e424b87e8cc45b24dd6be81787810123bb964a0df686f9
3
+ size 78829164
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy
2
+ opencv-python
3
+ transformers
4
+ accelerate
5
+ openai
6
+ google-cloud-videointelligence
7
+ google-cloud-speech
8
+ google-cloud-storage
9
+ ftfy
10
+ regex
11
+ tqdm
12
+ git+https://github.com/openai/CLIP.git
13
+ fastapi
14
+ pydantic
15
+ uvicorn
16
+ gradio
17
+
18
+
19
+
temporary_uploads/00.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5473e1871a2e3c7f53be879f770b3fca72cdc9af5d1080d3efdaeb7a47724c7e
3
+ size 1355860
temporary_uploads/01.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8e783983af3fd6f0c6acd4f5ed352f50924f091406d364db8d08169f25ff0e1f
3
+ size 10015373
temporary_uploads/02.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86084623354235419974b7bf756054f49b4974a31bd50d92b889e08605cae23d
3
+ size 905228
temporary_uploads/03.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e43f2f7bf476d6eb2c796a4ba3e8b605bba01e716cbcc370715f17874bbe5597
3
+ size 6804858
temporary_uploads/04.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9dff0ba8a9abfe3c89007450d799e3b273657d115109988ad2a84985e93c3005
3
+ size 2030868
temporary_uploads/05.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c78b5d4dc5b5ba772681770de8cb9d67e0a16eee8d41e7ed1629ffc2670a477
3
+ size 8746174
temporary_uploads/06.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f17d7bd78ef1cacaa7bceb5e4254f48192c9a5ca6a953a1fd8a832900cc2b8c
3
+ size 9650819
temporary_uploads/07.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b6b80848b779070f8c97ef005c78cec1cb38d7b1d8dc9dd693ada1d635412aa
3
+ size 6965241
temporary_uploads/08.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d76535828210f382ff1a90c4c075b9a371551a46ce27a29987f2d277fa487a24
3
+ size 3109225
temporary_uploads/09.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:646d9ba9f24b8230f095ac769e23c4593665c8ab12753b0a921f8bafa53d879c
3
+ size 3346334
temporary_uploads/10.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a826bbe9ef9fd5bfa6064d517c0c4bfb851699db8577aa55342fe90efd4d10e7
3
+ size 13774198
temporary_uploads/11.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:311934836f75574e812dc41ce76591f021e91550c620214989b79e6e142afe39
3
+ size 12821341
temporary_uploads/12.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc15a6c87991b2b3bad380e9a22179c10bc0928961ae69f01805fd02a09b33fb
3
+ size 4417086
temporary_uploads/13.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9150b291f38d04dffc5440e1732f21318338b8b385ed93eaea9b56aa843cb1b
3
+ size 2871413
temporary_uploads/14.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02650b0b36365220f232497e7d534f4a04979654e4bedb09295f750cd35e3147
3
+ size 6351275
temporary_uploads/15.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6dd0bb616575c9ee3f51651c9343c8a41c2d73bb33aa6182fad96375aa2a289c
3
+ size 9022185
temporary_uploads/16.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a7bd6ddc2b9640d0cec9a46c2405ce6fe3496fff8e9746cee5a06d92a0351a8
3
+ size 1594594
temporary_uploads/17.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5903a58ba48b14ae335686d65382155cf52d685d9d8b22f8eebcd5ce89a84513
3
+ size 8583185
temporary_uploads/18.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a30171ee97e016b89b8f52ccc11872fbbd7acac95088d0fdcc21f5cc2853bc2c
3
+ size 4493717
temporary_uploads/19.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:81a73c594d21071c11b902979f597a1c910dec26b565cfa0641fd16b90f0590f
3
+ size 19825821
temporary_uploads/20.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de3e3f50201bc119cf659aedab31fbc59b6a734cdcf84563939516b4f7170ae1
3
+ size 3318196
temporary_uploads/21.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bcb2d2b22305aa3617d1ce196dfe9112abe4f0ba238bc88e7b27965f7504c390
3
+ size 5656476
temporary_uploads/22.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7399d1c0ecfbfc7de160c5d221b78d89dfffccf5e88747a6abc44df03a6b0ad1
3
+ size 1519075
temporary_uploads/23.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fbf07a5151a000a888181f95b772f7e77895f549171dcca9f1d017b9d0918642
3
+ size 4944295
temporary_uploads/24.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b9d19eaa07c56633008fe2c6a8a2c983e76b42220a87c8d74dc717ef322ae65e
3
+ size 959905
temporary_uploads/25.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0192cab149d541a7f74b731296a95a5f2518c7509b8280802a0e9680dca06419
3
+ size 1587171
temporary_uploads/26.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb2369ee0cbafc47ddc7686936900f46ff4d34703c126e07703e8a8a90b5c692
3
+ size 839369
temporary_uploads/27.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aeb78d4b75f12b3de47ea6eb3261fb779996c1a4fd9d25fa01197c5488dceeef
3
+ size 4407540
temporary_uploads/28.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64dce12dfa07d38c4d04440a4216269d31bf727cd9fb95ee2aea79b6973b3d62
3
+ size 7621275
temporary_uploads/29.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46c9ad4d59dd50e373b96458553618e7370785b48e2c00a011b52968311c43bf
3
+ size 14509704
temporary_uploads/30.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:91c79a15fa1c7e7cf2c6145ea8d1693f144dedcfcc0931e3ba07fa7e8cb5c3d8
3
+ size 12327997
temporary_uploads/31.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:135fdd6b27b4815c67942391dbc3682669fd4f3c603263f9e58e8e4f14a24084
3
+ size 5777279
temporary_uploads/32.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:903c5b6a15ef492d9f09ba3bd2c776bbcd071280cf0fbe40807b8e0122d9e31e
3
+ size 19692437
temporary_uploads/33.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bbe568c2921d04a62347fd49ec7b3a49569999caacfbb3dd8b7ffa89574ae08
3
+ size 11676309
temporary_uploads/34.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3c2716a0f993f4f86d5435df6071f10e512a9412f7e3250933254f7f5b034748
3
+ size 9072592
temporary_uploads/35.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb400c187c9a8def2d88faddd7b0b8ee47604c19a824a42fea22ad6b6cecb983
3
+ size 2700713
temporary_uploads/36.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ceca619bb28d11339b10d9213d024486fad10bad7504a61d6f6aa8c38a060d62
3
+ size 2412048
temporary_uploads/37.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:78f8eafa868f0c45661196bf4cfce44fe6453e37c46dd985c606713d059c4418
3
+ size 727556
temporary_uploads/38.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ab5f78eba99e9ce66da9aa48d6e2bf49cc6855c3deaa7b467e5b97806e00e526
3
+ size 15636325
temporary_uploads/39.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19e6d09e98e145b9a6bf60f42f61c7c299e4aa202cde19f93eb0ee15b9d3a04e
3
+ size 2890293
temporary_uploads/40.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e63c994349b73089e3986c2fad5a998a20230e93ea8b8353a2b2407574cc3fa5
3
+ size 522480
temporary_uploads/41.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b744daab943541733de5293284b5accc97855bddcb5bd157f32271fd94c98c5
3
+ size 512357
temporary_uploads/42.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:922f7b881c7b17d40177775dfa4d16c64287e68264b62578fd18834175f1aaca
3
+ size 2953257
temporary_uploads/43.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad1749319f21293d29a94a6c1377b255cbd25382b6a5304251c7cf1a0c87483c
3
+ size 13226234