Skip to content

Commit

Permalink
feat: minor server improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfromyeg committed Jan 10, 2024
1 parent c06f0ff commit 0b04e30
Show file tree
Hide file tree
Showing 8 changed files with 29 additions and 220 deletions.
8 changes: 7 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
.github/
.mypy_cache/
.ruff_cache/
.vscode/

content/
client/
content/
env/
exports/

.editorconfig
.env
.env.example
.gitignore
.markdownlint.json
celery.log
docker-compose.local.yml
dumb.rdb
LICENSE
log.log
README.md
6 changes: 5 additions & 1 deletion bereal/send.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
For now, only phone. Eventually, consider e-mail.
"""
from .utils import TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER
from .utils import FLASK_ENV, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER
from .logger import logger

from twilio.rest import Client
Expand All @@ -16,6 +16,10 @@ def sms(phone: str, link: str) -> None:
try:
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

if FLASK_ENV == "development":
logger.info("Skipping SMS in development mode")
return

message_body = f"Here is the link to your BeReal Wrapped!\n\n{link}"
message = client.messages.create(body=message_body, from_=TWILIO_PHONE_NUMBER, to=phone)

Expand Down
12 changes: 7 additions & 5 deletions bereal/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .utils import ( # noqa: E402
CONTENT_PATH,
DEFAULT_SONG_PATH,
DEFAULT_SHORT_SONG_PATH,
EXPORTS_PATH,
FLASK_ENV,
GIT_COMMIT_HASH,
Expand All @@ -36,6 +37,7 @@
REDIS_HOST,
REDIS_PORT,
SECRET_KEY,
Mode,
str2mode,
)

Expand Down Expand Up @@ -123,10 +125,10 @@ def request_otp() -> tuple[Response, int]:
if otp_session is None:
return jsonify(
{
"error": "Bad Request",
"message": "Invalid phone number; BeReal is likely rate-limiting the service. Make sure not to include anything besides the digits (i.e., not '+' or '-' or spaces).",
"error": "Too Many Requests",
"message": "BeReal is rate-limiting your phone number. Please try again later.",
}
), 400
), 429

return jsonify({"otpSession": otp_session}), 200

Expand Down Expand Up @@ -185,10 +187,10 @@ def create_video() -> tuple[Response, int]:
wav_file.save(song_path)
except Exception as error:
logger.warning("Could not save music file, received: %s", error)
song_path = DEFAULT_SONG_PATH
song_path = DEFAULT_SHORT_SONG_PATH if mode == Mode.CLASSIC else DEFAULT_SONG_PATH
else:
logger.info("No music file provided; using default...")
song_path = DEFAULT_SONG_PATH
song_path = DEFAULT_SHORT_SONG_PATH if mode == Mode.CLASSIC else DEFAULT_SONG_PATH

logger.info("Queueing video task...")

Expand Down
File renamed without changes.
Binary file added bereal/static/songs/seven-nation-army.wav
Binary file not shown.
12 changes: 7 additions & 5 deletions bereal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def str2mode(s: str | None) -> Mode:
IMAGES_PATH = os.path.join(STATIC_PATH, "images")

SONGS_PATH = os.path.join(STATIC_PATH, "songs")
DEFAULT_SONG_PATH = os.path.join(SONGS_PATH, "midnight-city.wav")

DEFAULT_SONG_PATH = os.path.join(SONGS_PATH, "seven-nation-army.wav")
DEFAULT_SHORT_SONG_PATH = os.path.join(SONGS_PATH, "midnight-city-short.wav")

ENDCARD_TEMPLATE_IMAGE_PATH = os.path.join(IMAGES_PATH, "endCard_template.jpg")
ENDCARD_IMAGE_PATH = os.path.join(IMAGES_PATH, "endCard.jpg")
Expand All @@ -88,14 +90,14 @@ def str2mode(s: str | None) -> Mode:

config.read("config.ini")

HOST: str | None = os.getenv("HOST") or config.get("bereal", "host", fallback="localhost")
PORT: str | None = os.getenv("PORT") or config.get("bereal", "port", fallback="5000")
HOST: str | None = os.getenv("HOST") or "localhost"
PORT: str | None = os.getenv("PORT") or "5000"
PORT = int(PORT) if PORT is not None else None

TRUE_HOST = f"http://{HOST}:{PORT}" if FLASK_ENV == "development" else "https://api.bereal.michaeldemar.co"

REDIS_HOST: str | None = os.getenv("REDIS_HOST") or config.get("bereal", "redis_host", fallback="redis")
REDIS_PORT: str | None = os.getenv("REDIS_PORT") or config.get("bereal", "redis_port", fallback="6379")
REDIS_HOST: str | None = os.getenv("REDIS_HOST") or "redis"
REDIS_PORT: str | None = os.getenv("REDIS_PORT") or "6379"
REDIS_PORT = int(REDIS_PORT) if REDIS_PORT is not None else None

TIMEOUT = config.getint("bereal", "timeout", fallback=10)
Expand Down
205 changes: 3 additions & 202 deletions bereal/videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@
This script generates a slideshow from a folder of images and a music file.
"""
import os
from typing import Any, Generator
import gc

import librosa

from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.compositing.concatenate import concatenate_videoclips
from moviepy.video.fx import all as vfx
from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
from moviepy.video.VideoClip import VideoClip

from PIL import Image, ImageDraw, ImageFont

Expand All @@ -21,7 +17,6 @@
ENDCARD_TEMPLATE_IMAGE_PATH,
EXPORTS_PATH,
FONT_BASE_PATH,
IMAGE_EXTENSIONS,
IMAGE_QUALITY,
Mode,
)
Expand Down Expand Up @@ -61,203 +56,6 @@ def create_endcard(phone: str, year: str, n_images: int, font_size: int = 50, of
return encard_image_path


# deprecated, for now
def create_slideshow(
phone: str,
year: str,
input_folder: str,
output_file: str,
music_file: str | None,
timestamps: list[float],
mode: Mode = Mode.CLASSIC,
) -> None:
"""
Create a video slideshow from a target set of images.
"""
if not os.path.isdir(input_folder):
raise ValueError("Input folder does not exist!")

if music_file is not None and not os.path.isfile(music_file):
raise ValueError("Music file does not exist!")

image_paths = sorted(
[
os.path.join(input_folder, filename)
for filename in os.listdir(input_folder)
if any([filename.endswith(extension) for extension in IMAGE_EXTENSIONS])
]
)
n_images, n_timestamps = len(image_paths), len(timestamps)

logger.info("Processing %d images...", n_images)
logger.info("With %d timestamps...", n_timestamps)

if n_timestamps < n_images:
logger.info("Padding timestamps with last timestamps...")
timestamps += timestamps[: len(image_paths) - len(timestamps)]

# Create a clip for each image, with length determined by the timestamp
def generate_inner_clips() -> Generator[ImageSequenceClip, Any, None]:
for image_path, timestamp in zip(image_paths, timestamps):
logger.debug("Yielding clip %s", image_path)

clip = ImageSequenceClip([image_path], fps=1 / timestamp)
yield clip

# Append the end card
endcard_image_path = create_endcard(phone=phone, year=year, n_images=n_images)
endcard_clip = ImageSequenceClip([endcard_image_path], fps=1 / 3)

def generate_all_clips() -> Generator[ImageSequenceClip, Any, None]:
yield from generate_inner_clips()
yield endcard_clip

clips = list(generate_all_clips())
final_clip = concatenate_videoclips(clips=clips, method="compose")

# Optionally, trim to 30 seconds ("classic BeReal")
if mode == Mode.CLASSIC:
logger.info("Selected classic mode; clipping video to 30 seconds accordingly")
final_clip = final_clip.fx(
vfx.accel_decel,
new_duration=30,
)

# Optionally, add music
if music_file is not None:
music = AudioFileClip(music_file)
if music.duration < final_clip.duration:
logger.warning("Music is shorter than final clip; looping music")
music = music.fx(vfx.loop, duration=final_clip.duration)
else:
logger.info("Music is longer than final clip; clipping appropriately")
music = music.subclip(0, final_clip.duration)

music = music.audio_fadeout(3)

final_clip = final_clip.set_audio(music)

final_clip.write_videofile(output_file, codec="libx264", audio_codec="aac", threads=6, fps=24)


BATCH_SIZE = 10


def create_video_clip(image_path: str, timestamp: float) -> ImageSequenceClip:
"""
Create a video clip from a single image.
"""
return ImageSequenceClip([image_path], fps=1 / timestamp)


def process_batch(image_paths: list[str], timestamps: list[float]) -> VideoClip | None:
"""
Process a batch of images and create a concatenated video clip.
"""
clips: list[ImageSequenceClip] = [
create_video_clip(path, timestamp) for path, timestamp in zip(image_paths, timestamps)
]

if len(clips) == 0:
logger.warning("Empty batch; returning")
return None

vc = concatenate_videoclips(clips, method="compose")

clips.clear()
gc.collect()

return vc


def create_slideshow2(
phone: str,
year: str,
input_folder: str,
output_file: str,
music_file: str | None,
timestamps: list[float],
mode: Mode = Mode.CLASSIC,
) -> None:
"""
Create a video slideshow from a target set of images.
"""
if not os.path.isdir(input_folder):
raise ValueError("Input folder does not exist!")
if music_file is not None and not os.path.isfile(music_file):
raise ValueError("Music file does not exist!")

image_paths = sorted(
[
os.path.join(input_folder, f)
for f in os.listdir(input_folder)
if any(f.endswith(ext) for ext in IMAGE_EXTENSIONS)
]
)

logger.info("Have %d images with %d timestamps...", len(image_paths), len(timestamps))

if len(timestamps) < len(image_paths):
additional_needed = len(image_paths) - len(timestamps)

# Repeat the entire timestamps list as many times as needed
while additional_needed > 0:
timestamps.extend(timestamps[: min(len(timestamps), additional_needed)])
additional_needed = len(image_paths) - len(timestamps)

assert len(timestamps) >= len(image_paths)

logger.info("Padded %d images with %d timestamps...", len(image_paths), len(timestamps))

n_images = len(image_paths)
all_clips: list[VideoClip] = []
for i in range(0, n_images, BATCH_SIZE):
logger.info("Processing batch %d of %d", i // BATCH_SIZE, n_images // BATCH_SIZE)
logger.info("Images %d to %d", i, min(n_images, i + BATCH_SIZE))

batch_images = image_paths[i : min(n_images, i + BATCH_SIZE)]
batch_timestamps = timestamps[i : min(n_images, i + BATCH_SIZE)]

clip = process_batch(batch_images, batch_timestamps)

if clip is not None:
all_clips.append(clip)

endcard_image_path = create_endcard(phone=phone, year=year, n_images=n_images)
endcard_clip = ImageSequenceClip([endcard_image_path], fps=1 / 3)
all_clips.append(endcard_clip)

if len(all_clips) == 0:
logger.warning("No values in `all_clips`; returning")
return None

final_clip = concatenate_videoclips(all_clips, method="compose")

all_clips.clear()
gc.collect()

if mode == Mode.CLASSIC:
final_clip = final_clip.fx(
vfx.accel_decel,
new_duration=30,
)

if music_file is not None:
music = AudioFileClip(music_file)
if music.duration < final_clip.duration:
# TODO(michaelfromyeg): implement silence padding! (or maybe repeat clip...)
raise NotImplementedError("Music is shorter than final clip, not supported")
else:
logger.info("Music is longer than final clip; clipping appropriately")
music = music.subclip(0, final_clip.duration)
music = music.audio_fadeout(3)
final_clip = final_clip.set_audio(music)

final_clip.write_videofile(output_file, codec="libx264", audio_codec="aac", threads=4, fps=24)

return None


def create_slideshow3(
phone: str,
year: str,
Expand Down Expand Up @@ -303,11 +101,14 @@ def create_slideshow3(
main_clip = main_clip.fx(vfx.accel_decel, new_duration=30)

music = AudioFileClip(music_file)

if music.duration < main_clip.duration:
logger.warning("Music is shorter than final clip; looping music")

music = music.fx(vfx.loop, duration=main_clip.duration)
else:
logger.info("Music is longer than final clip; clipping appropriately")

music = music.subclip(0, main_clip.duration)

main_clip = main_clip.set_audio(music)
Expand Down
6 changes: 0 additions & 6 deletions config.ini
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
[bereal]
host=0.0.0.0
port=5000

redis_host="redis"
redis_port=6379

timeout=30
image_quality=20

0 comments on commit 0b04e30

Please sign in to comment.