Skip to content

Commit

Permalink
feat: more improvements; tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfromyeg committed Dec 31, 2023
1 parent 1007772 commit ef6adca
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"files.associations": {
"CNAME": "plaintext",
"*.yml": "yml",
"*.yml": "yaml",
},
"files.insertFinalNewline": true,
"editor.formatOnSave": true,
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN pip install --no-cache-dir -r requirements/prod.txt

RUN adduser --disabled-password --gecos '' thekid

RUN chown -R thekid:thekid /app && chmod -R 775 /app
RUN chown -R thekid:thekid /app && chmod -R 777 /app

USER thekid

Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ client:

build:
@echo "Building the server..."
@docker-compose build
@docker-compose -f docker-compose.local.yml build

up:
@echo "Booting up the server..."
@docker-compose up -d
@docker-compose -f docker-compose.local.yml up -d

down:
@echo "Shutting down the server..."
@docker-compose down
@docker-compose -f docker-compose.local.yml down

kill:
@echo "Killing the server..."
@docker-compose kill
@docker-compose -f docker-compose.local.yml kill

start-redis:
@echo "Booting up Redis..."
Expand Down
3 changes: 3 additions & 0 deletions bereal/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Celery stuff.
"""
import os
import gc
from celery import Celery

from .bereal import memories
Expand Down Expand Up @@ -49,13 +50,15 @@ def make_video(token: str, phone: str, year: str, song_path: str, mode: Mode) ->
image_folder = create_images(phone, year)
except Exception as e:
logger.error("Failed to create images: %s", e)
gc.collect()
raise e

logger.info("Creating video %s from %s...", video_file, image_folder)
try:
build_slideshow(phone, year, image_folder, song_path, video_file, mode)
except Exception as e:
logger.error("Failed to build slideshow: %s", e)
gc.collect()
raise e

logger.info("Cleaning up images")
Expand Down
4 changes: 2 additions & 2 deletions bereal/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from PIL import Image, ImageChops, ImageDraw, ImageFont

from .logger import logger
from .utils import CONTENT_PATH, FONT_BASE_PATH, OUTLINE_PATH
from .utils import CONTENT_PATH, FONT_BASE_PATH, OUTLINE_PATH, IMAGE_QUALITY


def process_image(
Expand Down Expand Up @@ -87,7 +87,7 @@ def process_image(

# Save the result in the output folder
output_path = os.path.join(output_folder, f"combined_{primary_filename}")
primary_image.save(output_path)
primary_image.save(output_path, quality=IMAGE_QUALITY)

logger.debug("Combined image saved at %s", output_path)

Expand Down
1 change: 1 addition & 0 deletions bereal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def str2mode(s: str | None) -> Mode:
REDIS_PORT = int(REDIS_PORT) if REDIS_PORT is not None else None

TIMEOUT = config.getint("bereal", "timeout", fallback=10)
IMAGE_QUALITY = config.getint("bereal", "image_quality", fallback=50)


# Utility methods
Expand Down
127 changes: 124 additions & 3 deletions bereal/videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"""
import os
from typing import Any, Generator
import gc

import librosa
from moviepy.editor import AudioFileClip, ImageSequenceClip, concatenate_videoclips, vfx
from moviepy.editor import AudioFileClip, ImageSequenceClip, VideoClip, concatenate_videoclips, vfx
from PIL import Image, ImageDraw, ImageFont

from .logger import logger
Expand All @@ -15,6 +16,7 @@
EXPORTS_PATH,
FONT_BASE_PATH,
IMAGE_EXTENSIONS,
IMAGE_QUALITY,
Mode,
)

Expand Down Expand Up @@ -46,11 +48,12 @@ def create_endcard(phone: str, year: str, n_images: int, font_size: int = 50, of
draw.text((x, y), text, font=font, fill="white")

encard_image_path = os.path.join(CONTENT_PATH, phone, year, "endcard.png")
img.save(encard_image_path)
img.save(encard_image_path, quality=IMAGE_QUALITY)

return encard_image_path


# deprecated, for now
def create_slideshow(
phone: str,
year: str,
Expand Down Expand Up @@ -133,6 +136,124 @@ def generate_all_clips() -> Generator[ImageSequenceClip, Any, None]:
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("Padded %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 convert_to_durations(timestamps: list[float]) -> list[float]:
"""
Calculate durations between consecutive timestamps.
Expand Down Expand Up @@ -171,7 +292,7 @@ def build_slideshow(
# logger.info("Skipping 'build_slideshow' stage; already created!")
# return None

create_slideshow(
create_slideshow2(
phone=phone,
year=year,
input_folder=image_folder,
Expand Down
28 changes: 25 additions & 3 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
}
}
41 changes: 39 additions & 2 deletions client/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,53 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { ToastContainer } from "react-toastify";
import { toast } from "react-toastify";
import axios from "axios";

import Footer from "./Footer";
import Form from "./Form";

import "../styles/App.css";

import { BASE_URL } from "../utils";

const App: React.FC = () => {
const [version, setVersion] = useState<string>("");

useEffect(() => {
const getStatus = async () => {
try {
const response = await axios.get(`${BASE_URL}/status`);

const data = response.data;

console.log(data);

setVersion(data.version);
} catch (error) {
console.error(error);

toast.error(
"There seems to be an issue with the server. Try again later.",
{
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
}
);
}
};

getStatus();
}, []);

return (
<div className="flex flex-col items-center">
<Form />
<Footer />
<Footer version={version} />
<ToastContainer />
</div>
);
Expand Down
Loading

0 comments on commit ef6adca

Please sign in to comment.