├── 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 | 5 | 6 | 7 | BeReel. | Failure 8 | 9 | 10 | 11 | 12 | 13 | 14 | 30 | 31 |
32 |

¯\_(ツ)_/¯

33 | 34 | 35 |

OK, something gone very wrong, check logs and logic

36 | 37 |
38 | 39 | 40 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /templates/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | BeReel. | Contact 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 |
33 |

Contact

34 |

Reach out to me at joshgonzales9891@gmail.com and check out my personal website at https://github.com/theOneAndOnlyOne.

35 |
36 | 37 | 38 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /templates/verify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BeReel. | Verification 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 |
30 |

Enter Verification Code

31 |
32 | 33 | 34 | 35 |
36 | {% if message %} 37 |

{{ message }}

38 | {% endif %} 39 |
40 | 41 | 42 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /templates/preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BeReel. | Preview 8 | 9 | 10 | 11 | 12 | 13 | 14 | 30 | 31 |
32 |

Preview

33 | 34 | 35 |
36 | 39 | 40 |
41 | 42 |
43 | 44 | 45 | 53 | 54 | 55 | 65 | 66 | -------------------------------------------------------------------------------- /templates/privacy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | BeReel. | Privacy Policy 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 |
33 |

Privacy Policy

34 |

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 | ![Welcome Screen](https://github.com/theOneAndOnlyOne/BeReel/blob/main/static/images/BeReal_Header.png) 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 | ![Video Settings](https://github.com/theOneAndOnlyOne/BeReel/blob/main/static/images/BeReel_Video_Settings.png) 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 |

(back to top)

63 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BeReel. | Start 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 |
30 |

Roll back the clock and relive your moments from BeReal. It's easy, fun, and a unique way to create a timelapse of all your posts.

31 |

Enter Phone Number

32 |
33 | × 34 | NOTE: Ensure you are entering your number with your area code in E.164 format 35 |
36 |
37 | 38 |
39 | 40 | 41 |
42 | {% if message %} 43 |

{{ message }}

44 | {% endif %} 45 |
46 |
47 | 48 | 49 | 57 | 58 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /templates/process.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BeReel. | Process Data 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 |
30 |

Video Settings

31 |

Verification Successful! Now please fill in the following information for the timelapse.

32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 | 45 | 46 |
47 |
48 | 49 | 53 |
54 |
55 | 56 | 59 |
60 |
61 |
62 | 63 | 64 | 72 | 73 | 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | BeReel. | About 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 |
33 |

About

34 |

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() --------------------------------------------------------------------------------