deepflow-engine 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepflow_engine-0.1.0/PKG-INFO +17 -0
- deepflow_engine-0.1.0/README.md +1 -0
- deepflow_engine-0.1.0/pyproject.toml +32 -0
- deepflow_engine-0.1.0/src/deepflow_engine/__init__.py +21 -0
- deepflow_engine-0.1.0/src/deepflow_engine/collisions.py +17 -0
- deepflow_engine-0.1.0/src/deepflow_engine/constants.py +17 -0
- deepflow_engine-0.1.0/src/deepflow_engine/errors/__init__.py +26 -0
- deepflow_engine-0.1.0/src/deepflow_engine/game.py +121 -0
- deepflow_engine-0.1.0/src/deepflow_engine/publisher/__init__.py +6 -0
- deepflow_engine-0.1.0/src/deepflow_engine/publisher/base.py +33 -0
- deepflow_engine-0.1.0/src/deepflow_engine/publisher/telegram.py +17 -0
- deepflow_engine-0.1.0/src/deepflow_engine/renderer.py +254 -0
- deepflow_engine-0.1.0/src/deepflow_engine/sample_game/__init__.py +1 -0
- deepflow_engine-0.1.0/src/deepflow_engine/sample_game/learning_pygame.py +40 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: deepflow-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automated frame-by-frame rendering and video synthesis for physics simulations and generative game shorts.
|
|
5
|
+
Author: deependujha
|
|
6
|
+
Author-email: deependujha <deependujha21@gmail.com>
|
|
7
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: moviepy>=2.2.1
|
|
12
|
+
Requires-Dist: pygame>=2.6.1
|
|
13
|
+
Requires-Dist: pytelegrambotapi>=4.32
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# DeepFlow-Engine
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DeepFlow-Engine
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = [ "uv-build>=0.10.8,<0.11" ]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "deepflow-engine"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
|
|
9
|
+
description = "Automated frame-by-frame rendering and video synthesis for physics simulations and generative game shorts."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "deependujha", email = "deependujha21@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"moviepy>=2.2.1",
|
|
23
|
+
"pygame>=2.6.1",
|
|
24
|
+
"pytelegrambotapi>=4.32",
|
|
25
|
+
]
|
|
26
|
+
scripts.deepflow-engine = "deepflow_engine:main"
|
|
27
|
+
scripts.deepflow_engine = "deepflow_engine:main"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"pre-commit>=4.5.1",
|
|
32
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
# from deepflow_engine.publisher.telegram import TelegramPublisher
|
|
4
|
+
# from deepflow_engine.renderer import video_renderer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
# publisher = TelegramPublisher()
|
|
9
|
+
# publisher.publish(
|
|
10
|
+
# "DeepFlow-Engine_sample.mp4", {"description": "A video generated by DeepFlow-Engine!"}
|
|
11
|
+
# )
|
|
12
|
+
# video_renderer(
|
|
13
|
+
# output_filename="DeepFlow-Engine_sample.mp4",
|
|
14
|
+
# frames_dir="silence/frames",
|
|
15
|
+
# collisions_log="silence/collisions_log.json",
|
|
16
|
+
# audio_dict={"crash": "silence/game-over.mp3"},
|
|
17
|
+
# )
|
|
18
|
+
print("Hello from DeepFlow-Engine!")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pipeline() -> None: ...
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Event:
|
|
8
|
+
# Example:
|
|
9
|
+
# {
|
|
10
|
+
# "frame": 143,
|
|
11
|
+
# "time": 2.3833333333333333,
|
|
12
|
+
# "type": "crash"
|
|
13
|
+
# }
|
|
14
|
+
|
|
15
|
+
frame: int
|
|
16
|
+
time: float
|
|
17
|
+
type: str
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
# Creating colors
|
|
4
|
+
BLUE = (0, 0, 255)
|
|
5
|
+
RED = (255, 0, 0)
|
|
6
|
+
GREEN = (0, 255, 0)
|
|
7
|
+
BLACK = (0, 0, 0)
|
|
8
|
+
WHITE = (255, 255, 255)
|
|
9
|
+
|
|
10
|
+
DEFAULT_GAME_WIDTH = 360
|
|
11
|
+
DEFAULT_GAME_HEIGHT = 640 # still 9:16
|
|
12
|
+
|
|
13
|
+
DEFAULT_WINDOW_WIDTH = 720
|
|
14
|
+
DEFAULT_WINDOW_HEIGHT = 1280
|
|
15
|
+
|
|
16
|
+
# --- PYGAME DEFAULT CONSTANTS ---
|
|
17
|
+
DEFAULT_FPS = 60
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseError(Exception):
|
|
5
|
+
"""Base class for all DeepFlow-Engine exceptions."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RendererError(BaseError):
|
|
11
|
+
"""Base class for all renderer-related exceptions."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str) -> None:
|
|
14
|
+
_base_msg = "An error occurred in the renderer: "
|
|
15
|
+
super().__init__(_base_msg + message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PublisherError(BaseError):
|
|
19
|
+
"""Base class for all publisher-related exceptions."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str) -> None:
|
|
22
|
+
_base_msg = "An error occurred in the publisher: "
|
|
23
|
+
super().__init__(_base_msg + message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["BaseError", "RendererError", "PublisherError"]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
import pygame
|
|
6
|
+
import sys
|
|
7
|
+
from typing import override
|
|
8
|
+
|
|
9
|
+
from deepflow_engine.constants import (
|
|
10
|
+
DEFAULT_FPS,
|
|
11
|
+
DEFAULT_GAME_HEIGHT,
|
|
12
|
+
DEFAULT_GAME_WIDTH,
|
|
13
|
+
WHITE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseGame(ABC):
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def start(self) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def exit(self) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def handle_events(self) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def update(self) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def render(self) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def loop(self) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DeepFlowEngineGame(BaseGame):
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
fps: int = DEFAULT_FPS,
|
|
51
|
+
caption: str = "DeepFlow-Engine",
|
|
52
|
+
display_surface_width: int = DEFAULT_GAME_WIDTH,
|
|
53
|
+
display_surface_height: int = DEFAULT_GAME_HEIGHT,
|
|
54
|
+
display_surface_fill_color: tuple[int, int, int] = WHITE,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__()
|
|
57
|
+
self._running = False
|
|
58
|
+
self.fps = fps
|
|
59
|
+
self.FramePerSec = pygame.time.Clock()
|
|
60
|
+
self.caption = caption
|
|
61
|
+
self.display_surface_width = display_surface_width
|
|
62
|
+
self.display_surface_height = display_surface_height
|
|
63
|
+
self.display_surface_fill_color = display_surface_fill_color
|
|
64
|
+
self.dt = 0.0
|
|
65
|
+
self._sprite_groups = {}
|
|
66
|
+
self._added_sprites = set()
|
|
67
|
+
|
|
68
|
+
def _verify_all_sprites_are_part_of_a_group(self) -> None:
|
|
69
|
+
for attribute, value in vars(self).items():
|
|
70
|
+
if (
|
|
71
|
+
isinstance(value, pygame.sprite.Sprite)
|
|
72
|
+
and value not in self._added_sprites
|
|
73
|
+
):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Sprite '{attribute}' is not part of any sprite group. Please add it to a sprite group using 'add_to_sprite_group' method."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def add_to_sprite_group(
|
|
79
|
+
self, group_name: str, sprite: pygame.sprite.Sprite
|
|
80
|
+
) -> None:
|
|
81
|
+
if group_name not in self._sprite_groups:
|
|
82
|
+
self._sprite_groups[group_name] = pygame.sprite.Group()
|
|
83
|
+
self._sprite_groups[group_name].add(sprite)
|
|
84
|
+
self._added_sprites.add(sprite)
|
|
85
|
+
|
|
86
|
+
def get_sprite_group(self, group_name: str) -> pygame.sprite.Group:
|
|
87
|
+
return self._sprite_groups.get(group_name, pygame.sprite.Group())
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def start(self) -> None:
|
|
91
|
+
self._verify_all_sprites_are_part_of_a_group()
|
|
92
|
+
pygame.init()
|
|
93
|
+
self._running = True
|
|
94
|
+
self.display_surface = pygame.display.set_mode(
|
|
95
|
+
(self.display_surface_width, self.display_surface_height)
|
|
96
|
+
)
|
|
97
|
+
self.display_surface.fill(self.display_surface_fill_color)
|
|
98
|
+
pygame.display.set_caption(self.caption)
|
|
99
|
+
|
|
100
|
+
@override
|
|
101
|
+
def loop(self) -> None:
|
|
102
|
+
while self._running:
|
|
103
|
+
self.handle_events()
|
|
104
|
+
self.update()
|
|
105
|
+
self.render()
|
|
106
|
+
# self.dt = self.FramePerSec.tick(self.fps) / 1000.0
|
|
107
|
+
self.FramePerSec.tick(self.fps)
|
|
108
|
+
self.dt = 1.0 / self.fps
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def render(self) -> None:
|
|
112
|
+
pygame.display.update()
|
|
113
|
+
|
|
114
|
+
@override
|
|
115
|
+
def exit(self) -> None:
|
|
116
|
+
# kill all sprites in the groups
|
|
117
|
+
for group in self._sprite_groups.values():
|
|
118
|
+
for sprite in group:
|
|
119
|
+
sprite.kill()
|
|
120
|
+
pygame.quit()
|
|
121
|
+
sys.exit()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from deepflow_engine.errors import PublisherError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class VideoMetadata:
|
|
10
|
+
description: str
|
|
11
|
+
tags: list[str]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BasePublisher(ABC):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def parse_metadata(self, metadata: VideoMetadata) -> str:
|
|
19
|
+
components = [f"Description: {metadata.description}"]
|
|
20
|
+
if metadata.tags:
|
|
21
|
+
components.append(f"Tags: {', '.join(metadata.tags)}")
|
|
22
|
+
return "\n".join(components)
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def _publish(self, filename: str, msg: str) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def publish(self, filename: str, metadata: VideoMetadata) -> None:
|
|
29
|
+
try:
|
|
30
|
+
parsed_metadata = self.parse_metadata(metadata)
|
|
31
|
+
self._publish(filename, parsed_metadata)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
raise PublisherError(str(e)) from e
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
import telebot
|
|
4
|
+
from typing import override
|
|
5
|
+
from deepflow_engine.publisher.base import BasePublisher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TelegramPublisher(BasePublisher):
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self._bot = telebot.TeleBot("8243264320:AAE_dZ-zje2lkKGeaqW3P5Z0Cp2vrITepcs")
|
|
11
|
+
self._chat_id = "-1003780272239" # oddly_realm group
|
|
12
|
+
|
|
13
|
+
@override
|
|
14
|
+
def _publish(self, filename: str, msg: str) -> None:
|
|
15
|
+
video = open(filename, "rb")
|
|
16
|
+
self._bot.send_video(chat_id=self._chat_id, video=video, caption=msg)
|
|
17
|
+
video.close()
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from deepflow_engine.errors import RendererError
|
|
10
|
+
from deepflow_engine.collisions import Event
|
|
11
|
+
from deepflow_engine.constants import DEFAULT_FPS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ----------------------------
|
|
15
|
+
# Frame Sorting
|
|
16
|
+
# ----------------------------
|
|
17
|
+
def get_sorted_base_frames(frames_dir: Path) -> list[Path]:
|
|
18
|
+
pattern = re.compile(r"^frame_(\d+)(?:_pause_(\d+))?\.png$")
|
|
19
|
+
frames = []
|
|
20
|
+
|
|
21
|
+
for f in frames_dir.iterdir():
|
|
22
|
+
m = pattern.match(f.name)
|
|
23
|
+
if m:
|
|
24
|
+
frame_num = int(m.group(1))
|
|
25
|
+
pause_num = int(m.group(2)) if m.group(2) is not None else -1
|
|
26
|
+
frames.append((frame_num, pause_num, f))
|
|
27
|
+
|
|
28
|
+
frames.sort(key=lambda x: (x[0], x[1]))
|
|
29
|
+
return [f for _, _, f in frames]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ----------------------------
|
|
33
|
+
# Load Events
|
|
34
|
+
# ----------------------------
|
|
35
|
+
def load_collisions(log_path: Path) -> set[Event]:
|
|
36
|
+
if not log_path.exists():
|
|
37
|
+
raise RendererError(f"{log_path} not found.")
|
|
38
|
+
|
|
39
|
+
with open(log_path) as fh:
|
|
40
|
+
data = json.load(fh)
|
|
41
|
+
|
|
42
|
+
return {Event(**entry) for entry in data}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ----------------------------
|
|
46
|
+
# Validation
|
|
47
|
+
# ----------------------------
|
|
48
|
+
def validate_audio_dict(audio_dict: dict[str, Path]) -> None:
|
|
49
|
+
for event_type, path in audio_dict.items():
|
|
50
|
+
if not path.exists():
|
|
51
|
+
raise RendererError(
|
|
52
|
+
f"Audio file for event '{event_type}' not found: {path}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_event_audio_mapping(
|
|
57
|
+
events: set[Event], audio_dict: dict[str, Path]
|
|
58
|
+
) -> None:
|
|
59
|
+
missing = {event.type for event in events if event.type not in audio_dict}
|
|
60
|
+
|
|
61
|
+
if missing:
|
|
62
|
+
raise RendererError(
|
|
63
|
+
f"Missing audio mapping for event types: {sorted(missing)}.\n"
|
|
64
|
+
f"Provided mappings: {list(audio_dict.keys())}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ----------------------------
|
|
69
|
+
# Build Frame Sequence
|
|
70
|
+
# ----------------------------
|
|
71
|
+
def build_frame_sequence(
|
|
72
|
+
base_frames: list[Path],
|
|
73
|
+
collision_frames: set[int],
|
|
74
|
+
pause_extra: int = 10,
|
|
75
|
+
) -> list[Path]:
|
|
76
|
+
sequence = []
|
|
77
|
+
frame_pattern = re.compile(r"^frame_(\d+)")
|
|
78
|
+
|
|
79
|
+
for frame_path in base_frames:
|
|
80
|
+
m = frame_pattern.match(frame_path.name)
|
|
81
|
+
if not m:
|
|
82
|
+
raise RendererError(f"Invalid frame filename: {frame_path.name}")
|
|
83
|
+
|
|
84
|
+
frame_num = int(m.group(1))
|
|
85
|
+
sequence.append(frame_path)
|
|
86
|
+
|
|
87
|
+
if frame_num in collision_frames:
|
|
88
|
+
sequence.extend([frame_path] * pause_extra)
|
|
89
|
+
|
|
90
|
+
return sequence
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ----------------------------
|
|
94
|
+
# Write FFmpeg Concat List
|
|
95
|
+
# ----------------------------
|
|
96
|
+
def write_concat_list(sequence: list[Path], list_path: Path, FPS: int) -> None:
|
|
97
|
+
duration = 1.0 / FPS
|
|
98
|
+
|
|
99
|
+
with open(list_path, "w") as fh:
|
|
100
|
+
for frame_path in sequence:
|
|
101
|
+
fh.write(f"file '{frame_path.resolve()}'\n")
|
|
102
|
+
fh.write(f"duration {duration:.10f}\n")
|
|
103
|
+
|
|
104
|
+
fh.write(f"file '{sequence[-1].resolve()}'\n")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ----------------------------
|
|
108
|
+
# FFmpeg Command Builder (Event-driven audio)
|
|
109
|
+
# ----------------------------
|
|
110
|
+
def build_ffmpeg_cmd(
|
|
111
|
+
concat_list: Path,
|
|
112
|
+
events: set[Event],
|
|
113
|
+
audio_dict: dict[str, Path],
|
|
114
|
+
video_duration: float,
|
|
115
|
+
output: Path,
|
|
116
|
+
) -> list[str]:
|
|
117
|
+
cmd = [
|
|
118
|
+
"ffmpeg",
|
|
119
|
+
"-y",
|
|
120
|
+
"-f",
|
|
121
|
+
"concat",
|
|
122
|
+
"-safe",
|
|
123
|
+
"0",
|
|
124
|
+
"-i",
|
|
125
|
+
str(concat_list),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# --- add audio inputs ---
|
|
129
|
+
audio_inputs = []
|
|
130
|
+
for event in events:
|
|
131
|
+
audio_path = audio_dict[event.type]
|
|
132
|
+
audio_inputs.append((event, audio_path))
|
|
133
|
+
|
|
134
|
+
for _, path in audio_inputs:
|
|
135
|
+
cmd += ["-i", str(path.resolve())]
|
|
136
|
+
|
|
137
|
+
# --- video encoding ---
|
|
138
|
+
cmd += [
|
|
139
|
+
"-fps_mode",
|
|
140
|
+
"passthrough",
|
|
141
|
+
"-pix_fmt",
|
|
142
|
+
"yuv420p",
|
|
143
|
+
"-c:v",
|
|
144
|
+
"libx264",
|
|
145
|
+
"-crf",
|
|
146
|
+
"18",
|
|
147
|
+
"-preset",
|
|
148
|
+
"slow",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# --- audio ---
|
|
152
|
+
if audio_inputs:
|
|
153
|
+
filter_parts = []
|
|
154
|
+
mix_inputs = []
|
|
155
|
+
|
|
156
|
+
for i, (event, _) in enumerate(audio_inputs):
|
|
157
|
+
delay_ms = int(event.time * 1000)
|
|
158
|
+
input_idx = i + 1
|
|
159
|
+
|
|
160
|
+
filter_parts.append(f"[{input_idx}:a]adelay={delay_ms}|{delay_ms}[a{i}]")
|
|
161
|
+
mix_inputs.append(f"[a{i}]")
|
|
162
|
+
|
|
163
|
+
filter_complex = (
|
|
164
|
+
";".join(filter_parts) + f";{''.join(mix_inputs)}"
|
|
165
|
+
f"amix=inputs={len(mix_inputs)}:duration=longest:normalize=1,"
|
|
166
|
+
f"apad,atrim=duration={video_duration:.6f}[a]"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
cmd += [
|
|
170
|
+
"-filter_complex",
|
|
171
|
+
filter_complex,
|
|
172
|
+
"-map",
|
|
173
|
+
"0:v:0",
|
|
174
|
+
"-map",
|
|
175
|
+
"[a]",
|
|
176
|
+
"-c:a",
|
|
177
|
+
"aac",
|
|
178
|
+
"-b:a",
|
|
179
|
+
"192k",
|
|
180
|
+
"-ar",
|
|
181
|
+
"44100",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
cmd.append(str(output))
|
|
185
|
+
return cmd
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ----------------------------
|
|
189
|
+
# Main Renderer
|
|
190
|
+
# ----------------------------
|
|
191
|
+
def video_renderer(
|
|
192
|
+
output_filename: str | Path,
|
|
193
|
+
frames_dir: str | Path,
|
|
194
|
+
collisions_log: str | Path,
|
|
195
|
+
audio_dict: dict[str, str | Path],
|
|
196
|
+
FPS: int = DEFAULT_FPS,
|
|
197
|
+
) -> Path:
|
|
198
|
+
"""Generate video from frames + event timeline + audio mapping."""
|
|
199
|
+
|
|
200
|
+
frames_dir = Path(frames_dir)
|
|
201
|
+
collisions_log = Path(collisions_log)
|
|
202
|
+
output_video = Path(output_filename)
|
|
203
|
+
audio_dict = {k: Path(v) for k, v in audio_dict.items()}
|
|
204
|
+
|
|
205
|
+
if not frames_dir.exists():
|
|
206
|
+
raise RendererError(f"frames directory '{frames_dir}' not found.")
|
|
207
|
+
|
|
208
|
+
base_frames = get_sorted_base_frames(frames_dir)
|
|
209
|
+
if not base_frames:
|
|
210
|
+
raise RendererError("no frames found.")
|
|
211
|
+
|
|
212
|
+
# --- events ---
|
|
213
|
+
collisions = load_collisions(collisions_log)
|
|
214
|
+
|
|
215
|
+
# --- validation ---
|
|
216
|
+
validate_audio_dict(audio_dict)
|
|
217
|
+
validate_event_audio_mapping(collisions, audio_dict)
|
|
218
|
+
|
|
219
|
+
collision_frame_nums = {event.frame for event in collisions}
|
|
220
|
+
|
|
221
|
+
# --- build timeline ---
|
|
222
|
+
sequence = build_frame_sequence(base_frames, collision_frame_nums)
|
|
223
|
+
|
|
224
|
+
if not sequence:
|
|
225
|
+
raise RendererError("empty frame sequence.")
|
|
226
|
+
|
|
227
|
+
video_duration = len(sequence) / FPS
|
|
228
|
+
|
|
229
|
+
# --- concat file ---
|
|
230
|
+
with tempfile.NamedTemporaryFile(
|
|
231
|
+
mode="w", suffix=".txt", delete=False, prefix="ffmpeg_concat_"
|
|
232
|
+
) as tmp:
|
|
233
|
+
concat_list = Path(tmp.name)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
write_concat_list(sequence, concat_list, FPS)
|
|
237
|
+
|
|
238
|
+
cmd = build_ffmpeg_cmd(
|
|
239
|
+
concat_list,
|
|
240
|
+
collisions,
|
|
241
|
+
audio_dict,
|
|
242
|
+
video_duration,
|
|
243
|
+
output_video,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
result = subprocess.run(cmd, check=False)
|
|
247
|
+
|
|
248
|
+
if result.returncode != 0:
|
|
249
|
+
raise RendererError(f"ffmpeg failed with code {result.returncode}")
|
|
250
|
+
|
|
251
|
+
return output_video.resolve()
|
|
252
|
+
|
|
253
|
+
finally:
|
|
254
|
+
concat_list.unlink(missing_ok=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# credits: https://github.com/deependujha
|
|
2
|
+
|
|
3
|
+
import pygame
|
|
4
|
+
from deepflow_engine.game import DeepFlowEngineGame
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimplePyGame(DeepFlowEngineGame):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__()
|
|
10
|
+
# Initializing
|
|
11
|
+
pygame.init()
|
|
12
|
+
|
|
13
|
+
# Setting up FPS
|
|
14
|
+
self.FPS = 60
|
|
15
|
+
self.FramePerSec = pygame.time.Clock()
|
|
16
|
+
|
|
17
|
+
# Creating colors
|
|
18
|
+
self.BLUE = (0, 0, 255)
|
|
19
|
+
self.RED = (255, 0, 0)
|
|
20
|
+
self.GREEN = (0, 255, 0)
|
|
21
|
+
self.BLACK = (0, 0, 0)
|
|
22
|
+
self.WHITE = (255, 255, 255)
|
|
23
|
+
|
|
24
|
+
# Other Variables for use in the program
|
|
25
|
+
self.SCREEN_WIDTH = 400
|
|
26
|
+
self.SCREEN_HEIGHT = 600
|
|
27
|
+
self.SPEED = 7
|
|
28
|
+
self.SCORE = 0
|
|
29
|
+
|
|
30
|
+
# Setting up Fonts
|
|
31
|
+
self.font = pygame.font.SysFont("Verdana", 60)
|
|
32
|
+
self.font_small = pygame.font.SysFont("Verdana", 20)
|
|
33
|
+
self.game_over = self.font.render("Game Over", True, self.BLACK)
|
|
34
|
+
|
|
35
|
+
self.background = pygame.image.load("AnimatedStreet.png")
|
|
36
|
+
|
|
37
|
+
# Create a white screen
|
|
38
|
+
self.DISPLAYSURF = pygame.display.set_mode((400, 600))
|
|
39
|
+
self.DISPLAYSURF.fill(self.WHITE)
|
|
40
|
+
pygame.display.set_caption("Game")
|