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.
- broadcaster_studio-0.1.0/PKG-INFO +18 -0
- broadcaster_studio-0.1.0/README.md +3 -0
- broadcaster_studio-0.1.0/broadcaster/__init__.py +4 -0
- broadcaster_studio-0.1.0/broadcaster/studio.py +180 -0
- broadcaster_studio-0.1.0/broadcaster_studio.egg-info/PKG-INFO +18 -0
- broadcaster_studio-0.1.0/broadcaster_studio.egg-info/SOURCES.txt +9 -0
- broadcaster_studio-0.1.0/broadcaster_studio.egg-info/dependency_links.txt +1 -0
- broadcaster_studio-0.1.0/broadcaster_studio.egg-info/requires.txt +1 -0
- broadcaster_studio-0.1.0/broadcaster_studio.egg-info/top_level.txt +1 -0
- broadcaster_studio-0.1.0/setup.cfg +4 -0
- broadcaster_studio-0.1.0/setup.py +15 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=9.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
broadcaster
|
|
@@ -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
|
+
)
|