├── static
├── favicon.ico
├── fonts
│ ├── Inter-Black.ttf
│ ├── Inter-Bold.ttf
│ ├── Inter-Light.ttf
│ ├── Inter-Thin.ttf
│ ├── Inter-Medium.ttf
│ ├── Inter-Regular.ttf
│ ├── Inter-ExtraBold.ttf
│ ├── Inter-ExtraLight.ttf
│ └── Inter-SemiBold.ttf
├── images
│ ├── BeReel_Logo.png
│ ├── BeReal_Header.png
│ ├── BeReel_Landing.png
│ ├── endCard_template.jpg
│ ├── BeReel_Video_Settings.png
│ └── secondary_image_outline.png
└── styles.css
├── requirements.txt
├── LICENSE
├── templates
├── failure.html
├── contact.html
├── verify.html
├── preview.html
├── privacy.html
├── index.html
├── process.html
└── about.html
├── README.md
├── combineImages.py
├── generateSlideshow.py
└── main.py
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/favicon.ico
--------------------------------------------------------------------------------
/static/fonts/Inter-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Black.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Light.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Thin.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Medium.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/static/images/BeReel_Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/BeReel_Logo.png
--------------------------------------------------------------------------------
/static/fonts/Inter-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-ExtraBold.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-ExtraLight.ttf
--------------------------------------------------------------------------------
/static/fonts/Inter-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/fonts/Inter-SemiBold.ttf
--------------------------------------------------------------------------------
/static/images/BeReal_Header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/BeReal_Header.png
--------------------------------------------------------------------------------
/static/images/BeReel_Landing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/BeReel_Landing.png
--------------------------------------------------------------------------------
/static/images/endCard_template.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/endCard_template.jpg
--------------------------------------------------------------------------------
/static/images/BeReel_Video_Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/BeReel_Video_Settings.png
--------------------------------------------------------------------------------
/static/images/secondary_image_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KaratekHD/BeReel/main/static/images/secondary_image_outline.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==3.0.0
2 | librosa==0.10.1
3 | moviepy==1.0.3
4 | numpy==1.26.2
5 | Pillow==10.1.0
6 | pydub==0.25.1
7 | Requests==2.31.0
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2023 Joshua Gonzales and others
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/templates/failure.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
BeReel was developed as an open-source app and gathers information from an unofficial BeReal API from chemokita13. BeReel has no association and responsibility with the API's development and how it accesses user information. See this link for more information about this API Project. This app is to be run locally as to comply with user security laws and privacy. Under no cases does this app store metadata elsewhere and all related images to develop the timelapse can be found in local folders labelled /primary /secondary /combined /static. All videos and images produced from this app is to be considered personal use and should only use accounts owned by the user.
35 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | # BeReel:
6 |
7 | Miss the Timelapse Recap feature from ReReal? Introducing BeReel. A Flask-based webtool that gives you a customized timelapse of your favourite BeReal memories.
8 |
9 | 
10 |
11 |
12 | * Implements a BeReal API to fetch user memories in a usable format
13 | * Fetch Memories at a specified date range
14 | * Renders using many open source libraries and fully customizable (with many features to come
15 | * Create a timelapsed that syncs with the .WAV audio
16 |
17 | ## Getting Started
18 |
19 | Follow these instructions to get your project up and running.
20 |
21 | ### Prerequisites
22 |
23 | Make sure you have the following installed on your machine:
24 |
25 | - [Python](https://www.python.org/downloads/) (Only compatible with versions <3.12)
26 | - [pip](https://pip.pypa.io/en/stable/installation/)
27 |
28 | ### Installing Dependencies
29 |
30 | Run all required libraries and run the app:
31 | ```bash
32 | pip install -r requirements.txt --user
33 | python main.py
34 | ```
35 | The Flask app will be available on [http://localhost:5000/](http://localhost:5000/). Multiple folders will be created to pull all image data from your memories
36 |
37 | ### Project Structure
38 |
39 | - main.py: Main flask app and drives webpage and API requests
40 | - combineImages.py: processes photos to be used for the slideshow
41 | - generateSlideshow.py: rendering timelapse video and audio
42 |
43 | ### Current Developments
44 |
45 | - [ ] Add 'no sound' option
46 | - [ ] Display RealMoji
47 | - [ ] Toggle Date Label setting
48 | - [ ] Show render progress from terminal->webpage
49 |
50 | ## Remarks
51 |
52 | This project wouldn't be here without the amazing work by [chemokita13](https://github.com/chemokita13/beReal-api). Please give him a star.
53 |
54 | This app is to be run locally as to comply with user security laws and privacy. Under no cases does this app store metadata elsewhere.
55 | The app utilizes this third-party API which may not be following terms set by BeReal, all videos and images produced from this app is to be considered personal use and should only use accounts owned by the user:
56 | If the company has particular issues, please submit a request via links in my profile.
57 |
58 | ## License
59 |
60 | Distributed under the MIT License. See `LICENSE.txt` for more information.
61 |
62 |
Hi, I'm Joshua Gonzales, a student passionate about web development and all other cool things. BeReel is a tool that displays user memories from the app BeReal in a timelapse. I created this site as a passion project after being an active user and wanted to make something special for new years.
35 |
FAQ
36 |
How does it work?
37 |
All user information is fetched by another open-source project by chemokita13
38 | by using an API to access endpoints from BeReal. It does most of the heavy lifting including user login, authentication, posting,
39 | feed information etc. It's an amazing project to look into and the creater is extremely talented so please give him a star.
40 | After downloading all memories of the user's choosing, BeReel process it all, adding effects, audio, proper timing and
41 | stores a video in your local computer. Check out the privacy policy page for more information.
42 |
43 |
44 |
My phone number doesn't work/can't get a verification code?
45 |
Check if the phone number you entered includes your area code in E.164 format with no dashes, spaces or unrelated characters.
46 | If you used BeReel repeatedly, please wait for a few minutes to request another verification code. I only tested the app with my
47 | own phone number so only North American phone numbers are the ones confirmed to be working. If you got errors, add a discussion on the git page and I'll
48 | see how I can help. 👍
49 |
50 |
51 |
Will this have a full web release on the internet?
52 |
Most likely not, sorry. There are a lot of resources involved that requires a lot of planning and infrastructure development to ensure data downloaded is stored
53 | securely for each and every user. It is better to be used as a local-run application for personal use. I may consider it in the future but as of now it remains an
54 | open-source passion project
55 |
56 |
57 |
dawg, who is that on the webpage favicon?
58 |
the goat
59 |
60 |
61 |
62 |
63 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/combineImages.py:
--------------------------------------------------------------------------------
1 | from PIL import Image, ImageDraw, ImageFont, ImageChops
2 | import os
3 |
4 | OUTLINE_PATH = f"static{os.path.sep}images{os.path.sep}secondary_image_outline.png"
5 | FONT_PATH = rf"static{os.path.sep}fonts{os.path.sep}Inter-Bold.ttf"
6 |
7 | def overlay_images(primary_folder, secondary_folder, output_folder):
8 |
9 | font_size = 50
10 | offset = 50
11 | text_opacity=150
12 |
13 | # Ensure output folder exists
14 | if not os.path.exists(output_folder):
15 | os.makedirs(output_folder)
16 |
17 | # Iterate through primary folder
18 | for primary_filename in os.listdir(primary_folder):
19 | # Extract prefix from primary filename
20 | primary_prefix = primary_filename.split('_')[0]
21 | date = primary_prefix
22 |
23 | # Check if there's a corresponding file in the secondary folder with the same prefix
24 | secondary_files = [file for file in os.listdir(secondary_folder) if file.startswith(primary_prefix)]
25 | if secondary_files:
26 | # Use the first matching file in the secondary folder
27 | secondary_filename = secondary_files[0]
28 |
29 | primary_path = os.path.join(primary_folder, primary_filename)
30 | secondary_path = os.path.join(secondary_folder, secondary_filename)
31 |
32 | # Load primary and secondary images
33 | # TO DO: Could be possible to pull these images directly from the API Endpoint, that way we won't have to use a database in the future
34 | primary_image = Image.open(primary_path)
35 | secondary_image = Image.open(secondary_path)
36 | source = Image.open(os.path.join(os.getcwd(), OUTLINE_PATH))
37 |
38 | primary_image = primary_image.convert("RGBA")
39 | secondary_image = secondary_image.convert("RGBA")
40 | source = source.convert("RGBA")
41 |
42 | #create border around secondary image
43 | secondary_image = ImageChops.multiply(source, secondary_image)
44 |
45 | # Resize secondary image to fraction the size of the primary image
46 | width, height = primary_image.size
47 | new_size = (width // 3, height // 3)
48 | secondary_image = secondary_image.resize(new_size)
49 |
50 | # Overlay secondary image on top-left corner of primary image
51 | primary_image.paste(secondary_image, (10, 10), secondary_image)
52 |
53 | # Get the image dimensions
54 | width, height = primary_image.size
55 | # Create a drawing object
56 | draw = ImageDraw.Draw(primary_image)
57 | # Choose a font (you may need to provide the path to a font file)static\fonts\Inter-SemiBold.ttf
58 | font = ImageFont.truetype(FONT_PATH, font_size)
59 | # Get the bounding box of the text
60 | text_bbox = draw.textbbox((0, 0), date, font=font)
61 | # Calculate the position to center the text
62 | x = (width - text_bbox[2]) // 2
63 | y = (height - text_bbox[3]) - offset
64 |
65 | # Calculate the size of the rectangle to fill the text_bbox
66 | rect_width = text_bbox[2] + 20 # Add some padding
67 | rect_height = text_bbox[3] + 20 # Add some padding
68 |
69 | # Draw a semi-transparent filled rectangle as the background
70 |
71 | # TO DO: So I just kinda eyeballed these offsets to make it fit, im too lazy to figure out the right ones automatically
72 | # To get picture perfect accuracy.
73 | draw.rectangle([(x - 30, y - 15), (x + rect_width + 10, y + rect_height + 10)], fill=(0, 0, 0, text_opacity))
74 |
75 | # Draw the text on the image
76 | draw.text((x, y), date, font=font, fill="white")
77 | # Save the modified image
78 |
79 | # Save the result in the output folder
80 | output_path = os.path.join(output_folder, f'combined_{primary_filename}')
81 | primary_image.save(output_path)
82 |
83 | print(f"Combined image saved at: {output_path}")
84 |
85 | def create_images():
86 | # Example usage
87 | primary_folder = os.path.join(os.getcwd(), 'primary')
88 | secondary_folder = os.path.join(os.getcwd(), 'secondary')
89 | output_folder = os.path.join(os.getcwd(), 'combined')
90 |
91 | overlay_images(primary_folder, secondary_folder, output_folder)
92 |
--------------------------------------------------------------------------------
/generateSlideshow.py:
--------------------------------------------------------------------------------
1 | from moviepy.editor import VideoFileClip, ImageSequenceClip, concatenate_videoclips, AudioFileClip, vfx
2 | from pydub import AudioSegment
3 | from PIL import Image, ImageDraw, ImageFont
4 | import numpy as np
5 | import os
6 | import librosa
7 |
8 | def create_endcard(num_memories, font_size=50, offset = 110):
9 |
10 | input_image_path = rf'static{os.path.sep}images{os.path.sep}endCard_template.jpg'
11 | output_image_path = rf'static{os.path.sep}images{os.path.sep}endCard.jpg'
12 |
13 | text = str(num_memories) + " memories and counting..."
14 |
15 | # Open the image
16 | img = Image.open(input_image_path)
17 | # Get the image dimensions
18 | width, height = img.size
19 | # Create a drawing object
20 | draw = ImageDraw.Draw(img)
21 | # Choose a font (you may need to provide the path to a font file)static\fonts\Inter-SemiBold.ttf
22 | font = ImageFont.truetype(rf'static{os.path.sep}fonts{os.path.sep}Inter-SemiBold.ttf', font_size)
23 | # Get the bounding box of the text
24 | text_bbox = draw.textbbox((0, 0), text, font=font)
25 | # Calculate the position to center the text
26 | x = (width - text_bbox[2]) // 2
27 | y = (height - text_bbox[3]) // 2 + offset
28 | # Draw the text on the image
29 | draw.text((x, y), text, font=font, fill="white")
30 | # Save the modified image
31 | img.save(output_image_path)
32 | return(output_image_path)
33 |
34 | def create_slideshow(input_folder, output_file, music_file, timestamps, mode = 'classic'):
35 | image_files = sorted([f for f in os.listdir(input_folder) if f.endswith(('.png', '.jpg', '.jpeg', 'webp'))])
36 |
37 | clips = []
38 | print("image file count: " + str(len(image_files)))
39 | print("beat count: " + str(len(timestamps)))
40 |
41 | # Check if timestamps has fewer values than imagefiles
42 | img_file_count = len(image_files)
43 | beat_count = len(timestamps)
44 | if len(timestamps) < len(image_files):
45 | # Adjust the length of timestamps by repeating its elements
46 | timestamps += timestamps[:len(image_files) - len(timestamps)]
47 | print("image file count: " + str(len(image_files)))
48 | print("beat count: " + str(len(timestamps)))
49 | for image_file, timestamp in zip(image_files, timestamps):
50 | image_path = os.path.join(input_folder, image_file)
51 | clip = ImageSequenceClip([image_path], fps=1 / timestamp)
52 | clips.append(clip)
53 | print("appended clip: ", image_path)
54 |
55 | endcard_img_path = create_endcard(len(image_files))
56 | endcard_clip = ImageSequenceClip([endcard_img_path], fps=1/3)
57 | clips.append(endcard_clip)
58 |
59 |
60 | final_clip = concatenate_videoclips(clips, method="compose")
61 |
62 | if mode == 'classic':
63 | print("clipping video to classic mode")
64 | final_clip = final_clip.fx(vfx.accel_decel, new_duration = 30)
65 |
66 | music = AudioFileClip(music_file)
67 | # Pad the audio with silence if it's shorter than the final clip
68 | if music.duration < final_clip.duration:
69 | print("music is shorter than final clip, will be padded with silence")
70 | #silence_duration = final_clip.duration - music.duration
71 | #silence = AudioSegment.silent(duration=silence_duration * 1000) # Duration in milliseconds
72 | #music += silence # Concatenate silence and audio
73 | else:
74 | print("music is longer than final clip, clipping")
75 | music = music.subclip(0, final_clip.duration)
76 | music = music.audio_fadeout(3)
77 | #print("Video Duration = ", final_clip.duration)
78 | #print("Music Duration = ", music_processed.duration)
79 |
80 | final_clip = final_clip.set_audio(music)
81 |
82 | final_clip.write_videofile(output_file, codec="libx264", audio_codec="aac", threads = 6, fps=24)
83 |
84 | def convert_to_durations(timestamps):
85 | durations = []
86 |
87 | # Calculate durations between consecutive timestamps
88 | for i in range(1, len(timestamps)):
89 | duration = timestamps[i] - timestamps[i - 1]
90 | durations.append(duration)
91 |
92 | return durations
93 |
94 | def buildSlideshow(mode = 'classic'):
95 | music = os.path.join(os.getcwd(), "curr_song.wav")
96 | print("loading music from ", music)
97 | audio_file = librosa.load(music)
98 | y, sr = audio_file
99 | tempo, beat_frames = librosa.beat.beat_track(y=y, sr=sr)
100 | beat_times_raw = librosa.frames_to_time(beat_frames,sr=sr)
101 | beat_times = [float(value) for value in beat_times_raw]
102 | beat_times = convert_to_durations(beat_times)
103 | print(beat_times)
104 |
105 | input_folder = os.path.join(os.getcwd(), 'combined')
106 | output_file = "static/slideshow_test.mp4"
107 |
108 | create_slideshow(input_folder, output_file, music, beat_times, mode)
109 |
--------------------------------------------------------------------------------
/static/styles.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Inter-SemiBold';
3 | src: url('/static/fonts/Inter-SemiBold.ttf') format('truetype');
4 | font-weight: 600;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: 'Inter-Regular';
10 | src: url('/static/fonts/Inter-Regular.ttf') format('truetype');
11 | font-weight: 400;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: 'Inter-Bold';
17 | src: url('/static/fonts/Inter-Bold.ttf') format('truetype');
18 | font-weight: 600;
19 | font-style: normal;
20 | }
21 |
22 | h1 {
23 | font-family: 'Inter-SemiBold',"Helvetica Neue",Helvetica,Arial,sans-serif;
24 | }
25 |
26 | body {
27 | font-family: 'Inter-Regular',"Helvetica Neue",Helvetica,Arial,sans-serif;
28 | margin: 0; /* Remove default body margin */
29 | display: block;
30 | align-items: center;
31 | justify-content: center;
32 | height: 100vh; /* Make the body take the full height of the viewport */
33 | }
34 |
35 | input {
36 | padding: 12px;
37 | border: 1px solid #ccc;
38 | border-radius: 4px;
39 | resize: vertical;
40 | }
41 |
42 | input[type="file"]::file-selector-button {
43 | border-radius: 4px;
44 | padding: 0 16px;
45 | height: 30px;
46 | cursor: pointer;
47 | background-color: white;
48 | border: 1px solid rgba(0, 0, 0, 0.16);
49 | box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05);
50 | margin-right: 16px;
51 | transition: background-color 200ms;
52 | }
53 |
54 | button {
55 | background-color: #222;
56 | border-radius: 4px;
57 | border-style: none;
58 | box-sizing: border-box;
59 | color: #fff;
60 | cursor: pointer;
61 | font-family: 'Inter-Regular',"Farfetch Basis","Helvetica Neue",Arial,sans-serif;
62 | font-weight: 700;
63 | line-height: 1.5;
64 | margin: 0;
65 | max-width: none;
66 | min-height: 44px;
67 | min-width: 10px;
68 | outline: none;
69 | overflow: hidden;
70 | padding: 9px 20px 8px;
71 | position: relative;
72 | text-align: center;
73 | text-transform: none;
74 | user-select: none;
75 | -webkit-user-select: none;
76 | touch-action: manipulation;
77 | }
78 |
79 | button:hover,
80 | button:focus {
81 | opacity: .75;
82 | }
83 |
84 | .container {
85 | max-width: 800px;
86 | padding: 60px;
87 | box-sizing: border-box;
88 | flex-direction: column;
89 | }
90 |
91 | .video-settings div{
92 | margin: 5px auto;
93 | }
94 |
95 | .mode-input-select {
96 | padding: 12px;
97 | border: 1px solid #ccc;
98 | border-radius: 4px;
99 | }
100 |
101 | .date-range {
102 | display: inline;
103 | }
104 |
105 | @media screen and (max-width: 600px) {
106 | .date-range {
107 | display: block;
108 | }
109 | }
110 |
111 | /* Top navigation bar styles */
112 | .navbar {
113 | background-color: #333;
114 | overflow: hidden;
115 | display: flex;
116 | justify-content: space-between;
117 | }
118 |
119 | .navbar a {
120 | color: #f2f2f2;
121 | text-align: center;
122 | padding: 14px 16px;
123 | text-decoration: none;
124 | border-radius: 2px;
125 | }
126 |
127 | .navbar a:hover {
128 | background-color: #ddd;
129 | color: black;
130 | }
131 |
132 | .active {
133 | background-color: #4CAF50;
134 | color: white;
135 | }
136 |
137 | /* Title text style */
138 |
139 | .navbar-title {
140 | display: flex;
141 | align-items: center; /* Center vertically */
142 | }
143 |
144 | .navbar-title img {
145 | margin-right: 8px; /* Add some spacing between the logo and text */
146 | padding-left: 60px;
147 | filter: invert(1);
148 | }
149 |
150 | .navbar-title-text {
151 | color: #f2f2f2;
152 | font-family: 'Inter-Bold',"Helvetica Neue",Helvetica,Arial,sans-serif;
153 | font-size: 24px;
154 | padding: 14px 0px;
155 | }
156 |
157 | .navbar-title img {
158 | margin-right: 8px; /* Add some spacing between the logo and text */
159 | }
160 |
161 | /* Navigation links container style */
162 | .navbar-links {
163 | display: flex;
164 | align-items: center; /* Center vertically */
165 | padding-right: 60px;
166 | }
167 |
168 | .navbar-links .icon {
169 | display: none;
170 | }
171 |
172 | @media screen and (max-width: 600px) {
173 | .navbar-title img {padding-left: 30px;}
174 | .navbar-links a {display: none;}
175 | .navbar-links a.icon {
176 | display: flex;
177 | float: right;
178 | }
179 | }
180 |
181 | @media screen and (max-width: 600px) {
182 | .navbar-links.responsive {position: relative;}
183 | .navbar-links.responsive .icon {
184 | position: absolute;
185 | right: 0;
186 | top: 0;
187 | }
188 | .navbar-links.responsive a {
189 | float: none;
190 | display: block;
191 | text-align: left;
192 | }
193 | }
194 | /* Video Element in preview.html */
195 |
196 | .video-preview {
197 | display: block;
198 | width: 564px;
199 | height: 752px;
200 | object-fit: cover;
201 | }
202 |
203 | @media screen and (max-width: 600px) {
204 | .video-preview{
205 | width: 300px;
206 | height: 400px;
207 | object-fit: cover;
208 | }
209 | }
210 |
211 | .download-button {
212 | background-color: #43abc9;
213 | margin-top: 15px;
214 | }
215 |
216 | /* The alert message box */
217 | .alert {
218 | padding: 20px;
219 | background-color: #43abc9;
220 | color: white;
221 | margin-bottom: 15px;
222 | }
223 |
224 | /* The close button */
225 | .closebtn {
226 | margin-left: 15px;
227 | color: white;
228 | font-weight: bold;
229 | float: right;
230 | font-size: 22px;
231 | line-height: 20px;
232 | cursor: pointer;
233 | transition: 0.3s;
234 | }
235 |
236 | /* When moving the mouse over the close button */
237 | .closebtn:hover {
238 | color: black;
239 | }
240 |
241 | /* Footer styles */
242 | .footer {
243 | text-align: center;
244 | padding: 20px 0px;
245 | color: #333;
246 | bottom: 0;
247 | width: 100%;
248 | position: fixed;
249 | }
250 |
251 | .footer-links a {
252 | color: #333;
253 | margin: 0 15px;
254 | text-decoration: none;
255 | }
256 |
257 | .footer-links a:hover {
258 | text-decoration: underline;
259 | }
260 |
261 |
262 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | from flask import Flask, render_template, request, jsonify
4 | from combineImages import create_images
5 | from generateSlideshow import buildSlideshow
6 | from datetime import datetime
7 |
8 | app = Flask(__name__, template_folder='templates')
9 |
10 | # Acquire Phone Number from User
11 | def send_code(phone):
12 | print("> Entered phone number is ", phone)
13 | # First Post to send out OTP session and code
14 | url_send_code = "https://berealapi.fly.dev/login/send-code"
15 |
16 | # IMPORTANT: Format must be +##########
17 | payload = {"phone": phone}
18 |
19 | print("-- Sending OTP Session Request --")
20 | response = requests.post(url_send_code, json=payload)
21 | otp_session = "n/a"
22 |
23 | if response.status_code == 201:
24 | print("> Request successful!")
25 | print("Response:", response.json())
26 | response_json = response.json()
27 | if "data" in response_json and "otpSession" in response_json["data"]:
28 | otp_session = response_json["data"]["otpSession"]
29 | print("OTP Session:", otp_session)
30 | else:
31 | print("No 'otpSession' found in the response.")
32 | else:
33 | print("Request failed with status code:", response.status_code)
34 | print(response.json())
35 |
36 | return otp_session
37 |
38 | # Verify Session using otp_session code and user entered otp_code recieved from phone notification
39 | def verify(otp_session, otp_code):
40 | #print("please enter OTP code")
41 | #otp_code = input()
42 | print("> OTP: ", otp_code)
43 |
44 | # Second POST request to verify base don user input
45 | url_verify = "https://berealapi.fly.dev/login/verify"
46 |
47 | payload_verify = {
48 | "code": otp_code,
49 | "otpSession": otp_session
50 | }
51 |
52 | print("-- Sending Verify Request --")
53 | response_verify = requests.post(url_verify, json=payload_verify)
54 | tokenObj = "n/a"
55 |
56 | if response_verify.status_code == 201:
57 | print("> Verification request successful!")
58 | print("Response:", response_verify.json())
59 | # Process the verification response if needed
60 | response_json = response_verify.json()
61 | if "data" in response_json and "token" in response_json["data"]:
62 | tokenObj = response_json["data"]["token"]
63 | print("tokenObj:", tokenObj)
64 | else:
65 | print("No 'tokenObj' found in the response.")
66 | exit()
67 | else:
68 | print("> Verification request failed with status code:", response_verify.status_code)
69 | print(response_verify.json())
70 | exit()
71 |
72 | return tokenObj
73 |
74 |
75 | #Fetch user memories. Skip to this stage if we already acquired reusable token
76 | def get_memories(tokenObj, start_date_range, end_date_range):
77 | url_mem_feed = "https://berealapi.fly.dev/friends/mem-feed"
78 | headers = {
79 | "token": tokenObj
80 | }
81 |
82 | # Create a folder named 'primary' if it doesn't exist
83 | folder_name = 'primary'
84 | if not os.path.exists(folder_name):
85 | os.makedirs(folder_name)
86 |
87 | # Create a folder named 'secondary' if it doesn't exist
88 | secondary_folder_name = 'secondary'
89 | if not os.path.exists(secondary_folder_name):
90 | os.makedirs(secondary_folder_name)
91 |
92 | print("-- Sending Get Memories Request --")
93 | response_mem_feed = requests.get(url_mem_feed, headers=headers)
94 | data_array = []
95 |
96 | if response_mem_feed.status_code == 200:
97 | print("> GET request successful!")
98 | # Process the response from mem-feed endpoint
99 | print("Response:", response_mem_feed.json())
100 | print("we did it yay")
101 | response_data = response_mem_feed.json().get('data', {})
102 | data_array = response_data.get('data', [])
103 |
104 | else:
105 | print("GET request failed with status code:", response_mem_feed.status_code)
106 |
107 |
108 | start_date_str = str(start_date_range)
109 | end_date_str = str(end_date_range)
110 | # Convert the input strings to datetime objects
111 | start_date_object = datetime.strptime(start_date_str, '%Y-%m-%d')
112 | end_date_object = datetime.strptime(end_date_str, '%Y-%m-%d')
113 |
114 | # Iterate through the 'data' array and download images
115 | for item in data_array:
116 | image_url = item['primary'].get('url', '')
117 | secondary_image_url = item['secondary'].get('url', '')
118 | date = item['memoryDay']
119 | date_object = datetime.strptime(date, '%Y-%m-%d')
120 |
121 | if image_url and start_date_object <= date_object <= end_date_object:
122 | # Extracting the image name from the URL
123 | image_name = date + "_" + image_url.split('/')[-1]
124 | # Downloading the image
125 | image_path = os.path.join(folder_name, image_name)
126 | with open(image_path, 'wb') as img_file:
127 | img_response = requests.get(image_url)
128 | if img_response.status_code == 200:
129 | img_file.write(img_response.content)
130 | print(f"Downloaded {image_name} to {folder_name}")
131 | else:
132 | print(f"Failed to download {image_name}")
133 | if secondary_image_url and start_date_object <= date_object <= end_date_object:
134 | # Extracting the image name from the URL
135 | image_name = date + "_" + secondary_image_url.split('/')[-1]
136 | # Downloading the image
137 | image_path = os.path.join(secondary_folder_name, image_name)
138 | with open(image_path, 'wb') as img_file:
139 | img_response = requests.get(secondary_image_url)
140 | if img_response.status_code == 200:
141 | img_file.write(img_response.content)
142 | print(f"Downloaded {image_name} to {secondary_folder_name}")
143 | else:
144 | print(f"Failed to download {image_name}")
145 |
146 | return "complete"
147 | # All images referenced in the 'primary' URLs should now be saved in the 'primary' folder
148 | # 'secondary' URLS saved in 'secondary', etc.
149 |
150 |
151 | #-------------------------------------------------------------------------------------------------------------------------
152 | # Flask App Routing
153 |
154 | @app.route('/', methods=['GET', 'POST'])
155 | def index():
156 | if request.method == 'POST':
157 | phone_number = request.form['phone_number']
158 | otp_session = send_code(phone_number)
159 |
160 | if otp_session != 'n/a':
161 | return render_template('verify.html', otp_session=otp_session)
162 |
163 | return render_template('index.html', message='Invalid phone number. Check formatting and Please try again.')
164 |
165 | return render_template('index.html')
166 |
167 | @app.route('/verify', methods=['POST'])
168 | def verify_code():
169 | if request.method == 'POST':
170 | user_code = request.form['verification_code']
171 | otp_session = request.form['otp_session']
172 | print("> verify_code otp_session: ", otp_session)
173 | tokenObj = verify(otp_session, user_code)
174 |
175 | if tokenObj != 'n/a':
176 | return render_template('process.html', tokenObj=tokenObj)
177 |
178 | else:
179 | return render_template('failure.html')
180 | #return render_template('verify.html', tokenObj='n/a', message='Invalid verification code. Please try again.')
181 |
182 | return render_template('verify.html')
183 |
184 | @app.route('/process', methods=['POST'])
185 | def process_data():
186 | if request.method == 'POST':
187 | start_date_range = request.form['start_date_range']
188 | end_date_range = request.form['end_date_range']
189 | wav_file = request.files['wav_file']
190 | tokenObj = request.form['tokenObj']
191 | mode = request.form.get('mode')
192 |
193 | print("> HTML Form Elements: ")
194 | print("start_date_range ",str(start_date_range))
195 | print("end_date_range ",str(end_date_range))
196 | print("wav_file ",str(wav_file))
197 | print("mode",str(mode))
198 | # Call get_memories function
199 |
200 | print("> donwloading music file locally: ")
201 | try:
202 | # Save the uploaded WAV file locally
203 | upload_directory = os.getcwd()
204 | print("saving file to ", upload_directory)
205 | if not os.path.exists(upload_directory):
206 | os.makedirs(upload_directory)
207 |
208 | wav_file.save(os.path.join(upload_directory, "curr_song.wav"))
209 |
210 | except Exception as e:
211 | print(f"Error in processing data: {str(e)}")
212 |
213 | print("> downloading images locally")
214 | result = get_memories(tokenObj, start_date_range, end_date_range)
215 |
216 | if result != 'n/a':
217 | # Execute the Python functions
218 | create_images() # process images and apply effects
219 | # do something with current page here
220 | buildSlideshow(mode) # assemble files and load audio
221 | # do something with current page here
222 | return render_template('preview.html')
223 | else:
224 | return render_template('failure.html')
225 |
226 |
227 | return render_template('process.html')
228 |
229 | #@app.route('/run-python-functions', methods=['POST'])
230 | #def run_python_functions():
231 | # try:
232 | # # Execute the Python functions
233 | # create_images() # process images and apply effects
234 | # buildSlideshow() # assemble files and load audio
235 | #
236 | # return render_template('preview.html') # Success! redirect to preview page
237 | #
238 | # except Exception as e:
239 | # # Return a JSON response indicating failure
240 | # return render_template('failure.html', message=str(e)) # Failure! redirect to failure page
241 |
242 | @app.route('/about')
243 | def about():
244 | return render_template('about.html')
245 |
246 | @app.route('/privacy')
247 | def privacy():
248 | return render_template('privacy.html')
249 |
250 | @app.route('/contact')
251 | def contact():
252 | return render_template('contact.html')
253 |
254 | @app.route('/preview')
255 | def preview():
256 | return render_template('preview.html')
257 |
258 | @app.route('/failure')
259 | def failure():
260 | return render_template('failure.html')
261 |
262 | if __name__ == '__main__':
263 | app.run(debug=True)
264 |
265 | #otp_session = send_code()
266 | #tokenObj = verify(otp_session)
267 | #get_memories(tokenObj)
268 | #create_images()
269 | #buildSlideshow()
--------------------------------------------------------------------------------