deepflow-engine 0.1.0__py3-none-any.whl

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.
@@ -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,6 @@
1
+ # credits: https://github.com/deependujha
2
+
3
+ from deepflow_engine.publisher.base import BasePublisher
4
+ from deepflow_engine.publisher.telegram import TelegramPublisher
5
+
6
+ __all__ = ["BasePublisher", "TelegramPublisher"]
@@ -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")
@@ -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,15 @@
1
+ deepflow_engine/__init__.py,sha256=lbWKFGT5X6vRu0gCCW-gaVwNoNMukkWtefSK93v2F2w,663
2
+ deepflow_engine/collisions.py,sha256=GkGtVoQwtNfgXbfa4HfiUqafQHfmxTeWTaHSpk6zVkw,285
3
+ deepflow_engine/constants.py,sha256=zfVnSTb7i6k6OGqSdj3yFoLXRX-e8tCOTFMzFKwNlCg,336
4
+ deepflow_engine/errors/__init__.py,sha256=jSVMuEQY_euLT67nNwpKEta_SuSy5ofCX3eWpJYb-tM,689
5
+ deepflow_engine/game.py,sha256=NofAl0B5W0gxwee448R3ftECczVmQCqK1HFGqNUAH5A,3434
6
+ deepflow_engine/publisher/__init__.py,sha256=JsNDI-NQvOBVTNFThlQE51rjak3781PqQMoOllJz4HY,215
7
+ deepflow_engine/publisher/base.py,sha256=1k4AdqfCatkY9HoSJCrH7HN914QIl2QZvQQPTJ93lDU,934
8
+ deepflow_engine/publisher/telegram.py,sha256=cahXq88dxCSMEe3cf6tXhhckOMuEbQLJ2ocvb4RB878,574
9
+ deepflow_engine/renderer.py,sha256=9OgV_WAeKqs5XydpyV8PgzzL9k8EDx_RIN2v0fTnqzw,6679
10
+ deepflow_engine/sample_game/__init__.py,sha256=6wRYwKripAHu5kFdzP77BcGZLPh5MnwLBvvGif9o6KA,42
11
+ deepflow_engine/sample_game/learning_pygame.py,sha256=R7lSs83V31ZeQUuBbElfBnfvbSwa-gMw_0njFdDmDkA,1159
12
+ deepflow_engine-0.1.0.dist-info/WHEEL,sha256=GuAqCqoyQuys5_R4zkHUJFlKXw4RpRLNzo31-ui90WQ,81
13
+ deepflow_engine-0.1.0.dist-info/entry_points.txt,sha256=CNT6Xr2Md_j57-v4OkgZfATNOC1qcVZs4NZOb-gjkRA,97
14
+ deepflow_engine-0.1.0.dist-info/METADATA,sha256=lvQtIEr6UqPQOYVrY7cdHJWFmBFGZZijF96g0Xj54gw,636
15
+ deepflow_engine-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.12
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ deepflow-engine = deepflow_engine:main
3
+ deepflow_engine = deepflow_engine:main
4
+