broadcaster-studio 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.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: broadcaster-studio
3
+ Version: 0.1.0
4
+ Summary: FFmpeg & Pillow based Ultra-fast Broadcast Studio Overlay Tool
5
+ Author: KwonPop
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: Pillow>=9.0.0
9
+ Dynamic: author
10
+ Dynamic: description
11
+ Dynamic: description-content-type
12
+ Dynamic: requires-dist
13
+ Dynamic: requires-python
14
+ Dynamic: summary
15
+
16
+ # 🎬 Broadcaster Studio
17
+
18
+ FFmpeg + Pillow 기반 방송 오버레이 파이썬 라이브러리
@@ -0,0 +1,3 @@
1
+ # 🎬 Broadcaster Studio
2
+
3
+ FFmpeg + Pillow 기반 방송 오버레이 파이썬 라이브러리
@@ -0,0 +1,4 @@
1
+ from .studio import BroadcasterStudio, BroadcastPreset
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["BroadcasterStudio", "BroadcastPreset"]
@@ -0,0 +1,180 @@
1
+ import subprocess
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from PIL import Image, ImageDraw, ImageFont
6
+
7
+ class BroadcastPreset:
8
+ NEWS = {
9
+ "banner_bg": (15, 23, 42, 230),
10
+ "category_bg": (225, 29, 72, 255),
11
+ "text_color": "white",
12
+ "live_bg": (220, 38, 38, 255)
13
+ }
14
+ ENTERTAINMENT = {
15
+ "banner_bg": (236, 72, 153, 230),
16
+ "category_bg": (250, 204, 21, 255),
17
+ "text_color": "black",
18
+ "live_bg": (168, 85, 247, 255)
19
+ }
20
+ DOCUMENTARY = {
21
+ "banner_bg": (255, 255, 255, 180),
22
+ "category_bg": (30, 41, 59, 255),
23
+ "text_color": "white",
24
+ "live_bg": (59, 130, 246, 255)
25
+ }
26
+
27
+ class BroadcasterStudio:
28
+ def __init__(self, input_video: str, resolution=(1920, 1080)):
29
+ self.input_video = input_video
30
+ self.width, self.height = resolution
31
+ self.temp_audio = "_temp_audio.aac"
32
+ self.temp_main_overlay = "_temp_main_overlay.png"
33
+ self.temp_age_badge = "_temp_age_badge.png"
34
+
35
+ self.preset = BroadcastPreset.NEWS
36
+ self.ticker_text = ""
37
+ self.category_title = "[속보]"
38
+ self.show_live = False
39
+ self.show_clock = False
40
+ self.age_rating = None
41
+
42
+ self.font_path = self._detect_termux_font()
43
+
44
+ def _detect_termux_font(self) -> str:
45
+ home = str(Path.home())
46
+ candidates = [
47
+ os.path.join(home, ".fonts", "NotoSansKR-Bold.ttf"),
48
+ os.path.join(home, ".fonts", "NotoSansKR-Bold.otf"),
49
+ "/data/data/com.termux/files/usr/share/fonts/TTF/NotoSansCJK-Bold.ttc",
50
+ "/data/data/com.termux/files/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
51
+ "/system/fonts/NotoSansCJK-Regular.ttc",
52
+ "/system/fonts/DroidSansFallback.ttf"
53
+ ]
54
+ for path in candidates:
55
+ if os.path.exists(path):
56
+ print(f"✅ 폰트 감지 성공: {path}")
57
+ return path
58
+ print("⚠️ 한글 폰트를 찾지 못했습니다!")
59
+ return ""
60
+
61
+ def set_preset(self, preset_type: str):
62
+ preset_key = preset_type.upper()
63
+ if hasattr(BroadcastPreset, preset_key):
64
+ self.preset = getattr(BroadcastPreset, preset_key)
65
+ return self
66
+
67
+ def add_news_ticker(self, category="[속보]", text="자막 내용을 입력하세요."):
68
+ self.category_title = category
69
+ self.ticker_text = text
70
+ return self
71
+
72
+ def add_live_badge(self, enable=True):
73
+ self.show_live = enable
74
+ return self
75
+
76
+ def add_broadcast_clock(self, enable=True):
77
+ self.show_clock = enable
78
+ return self
79
+
80
+ def set_age_rating(self, age: int):
81
+ self.age_rating = age
82
+ return self
83
+
84
+ def _create_static_overlays(self):
85
+ img = Image.new("RGBA", (self.width, self.height), (0, 0, 0, 0))
86
+ draw = ImageDraw.Draw(img)
87
+
88
+ banner_h = 140
89
+ banner_y = self.height - banner_h
90
+ draw.rectangle([0, banner_y, self.width, self.height], fill=self.preset["banner_bg"])
91
+
92
+ cat_w = 300
93
+ draw.rectangle([0, banner_y, cat_w, self.height], fill=self.preset["category_bg"])
94
+
95
+ if self.show_live:
96
+ draw.rounded_rectangle([self.width - 200, 40, self.width - 40, 90], radius=12, fill=self.preset["live_bg"])
97
+ draw.ellipse([self.width - 180, 58, self.width - 166, 72], fill="white")
98
+
99
+ img.save(self.temp_main_overlay)
100
+
101
+ if self.age_rating:
102
+ badge_img = Image.new("RGBA", (120, 120), (0, 0, 0, 0))
103
+ b_draw = ImageDraw.Draw(badge_img)
104
+ circle_color = (220, 38, 38, 240) if self.age_rating >= 19 else (234, 179, 8, 240)
105
+ b_draw.ellipse([5, 5, 115, 115], fill=circle_color, outline=(255, 255, 255, 255), width=4)
106
+ badge_img.save(self.temp_age_badge)
107
+
108
+ def _extract_audio(self):
109
+ cmd = [
110
+ "ffmpeg", "-y",
111
+ "-i", self.input_video,
112
+ "-vn", "-c:a", "copy",
113
+ self.temp_audio
114
+ ]
115
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
116
+
117
+ def render(self, output_path="final_broadcast.mp4"):
118
+ print("🎬 [1/3] 자막 템플릿 및 폰트 리소스 준비 중...")
119
+ self._extract_audio()
120
+ self._create_static_overlays()
121
+
122
+ print("🚀 [2/3] FFmpeg 엔진 가동...")
123
+
124
+ inputs = [
125
+ "-i", self.input_video,
126
+ "-i", self.temp_main_overlay,
127
+ ]
128
+
129
+ filter_chains = []
130
+ filter_chains.append("[0:v][1:v]overlay=0:0[v1]")
131
+ current_v = "[v1]"
132
+
133
+ if self.age_rating and os.path.exists(self.temp_age_badge):
134
+ inputs.extend(["-i", self.temp_age_badge])
135
+ filter_chains.append(f"{current_v}[2:v]overlay=50:50:enable='between(t,0,5)'[v2]")
136
+ current_v = "[v2]"
137
+
138
+ font_opt = f"fontfile='{self.font_path}'" if self.font_path else ""
139
+
140
+ filter_chains.append(
141
+ f"{current_v}drawtext={font_opt}:text='{self.category_title}':x=40:y=H-95:"
142
+ f"fontsize=42:fontcolor=white:bold=1[v3]"
143
+ )
144
+ current_v = "[v3]"
145
+
146
+ if self.ticker_text:
147
+ filter_chains.append(
148
+ f"{current_v}drawtext={font_opt}:text='{self.ticker_text}':"
149
+ f"x='w-mod(t*200\\, w+tw)':y=H-90:fontsize=36:fontcolor=white[v4]"
150
+ )
151
+ current_v = "[v4]"
152
+
153
+ if self.show_clock:
154
+ filter_chains.append(
155
+ f"{current_v}drawtext={font_opt}:text='%{{pts\\:hms}}':"
156
+ f"x=W-220:y=H-120:fontsize=28:fontcolor=yellow:box=1:boxcolor=black@0.6[v5]"
157
+ )
158
+ current_v = "[v5]"
159
+
160
+ inputs.extend(["-i", self.temp_audio])
161
+ audio_idx = (len(inputs) // 2) - 1
162
+
163
+ cmd = ["ffmpeg", "-y"] + inputs + [
164
+ "-filter_complex", ";".join(filter_chains),
165
+ "-map", current_v,
166
+ "-map", f"{audio_idx}:a?",
167
+ "-c:v", "libx264",
168
+ "-preset", "ultrafast",
169
+ "-c:a", "copy",
170
+ output_path
171
+ ]
172
+
173
+ subprocess.run(cmd, check=True)
174
+ self.cleanup()
175
+ print(f"🎉 [3/3] 방송 합성 성공!! 출력 파일: {output_path}")
176
+
177
+ def cleanup(self):
178
+ for temp in [self.temp_audio, self.temp_main_overlay, self.temp_age_badge]:
179
+ if os.path.exists(temp):
180
+ os.remove(temp)
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: broadcaster-studio
3
+ Version: 0.1.0
4
+ Summary: FFmpeg & Pillow based Ultra-fast Broadcast Studio Overlay Tool
5
+ Author: KwonPop
6
+ Requires-Python: >=3.7
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: Pillow>=9.0.0
9
+ Dynamic: author
10
+ Dynamic: description
11
+ Dynamic: description-content-type
12
+ Dynamic: requires-dist
13
+ Dynamic: requires-python
14
+ Dynamic: summary
15
+
16
+ # 🎬 Broadcaster Studio
17
+
18
+ FFmpeg + Pillow 기반 방송 오버레이 파이썬 라이브러리
@@ -0,0 +1,9 @@
1
+ README.md
2
+ setup.py
3
+ broadcaster/__init__.py
4
+ broadcaster/studio.py
5
+ broadcaster_studio.egg-info/PKG-INFO
6
+ broadcaster_studio.egg-info/SOURCES.txt
7
+ broadcaster_studio.egg-info/dependency_links.txt
8
+ broadcaster_studio.egg-info/requires.txt
9
+ broadcaster_studio.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="broadcaster-studio",
5
+ version="0.1.0",
6
+ description="FFmpeg & Pillow based Ultra-fast Broadcast Studio Overlay Tool",
7
+ long_description=open("README.md", encoding="utf-8").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="KwonPop",
10
+ packages=find_packages(),
11
+ install_requires=[
12
+ "Pillow>=9.0.0",
13
+ ],
14
+ python_requires=">=3.7",
15
+ )