-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcontent_producer.py
186 lines (150 loc) · 6.93 KB
/
content_producer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import os
import elevenlabs
import assemblyai as aai
from dotenv import load_dotenv
from moviepy.editor import (
VideoFileClip,
concatenate_videoclips,
CompositeVideoClip,
AudioFileClip,
TextClip,
ColorClip,
)
from moviepy.video.tools.subtitles import SubtitlesClip
from elevenlabs import Voice, VoiceSettings
from elevenlabs.client import ElevenLabs
class ContentError(Exception):
"""Exception raised for errors in the ContentProducer class."""
pass
class ContentData:
"""A class for storing content data."""
def __init__(self, category, script, montage_url):
"""Initializes the ContentData object.
Args:
category (str): The category of the content.
script (str): The script for the content.
montage_url (str): The URL of the montage video for the content.
"""
self.category = category
self.script = script
self.montage_url = montage_url
def __str__(self):
"""Returns a string representation of the ContentData object."""
return f"Category: {self.category}, Script: {self.script}, Montage URL: {self.montage_url}"
class ContentProducer:
"""A class for producing content from provided content data."""
def __init__(self):
"""Initializes the ContentProducer."""
load_dotenv()
self.assembly_ai_api_key = os.environ.get("ASSEMBLY_AI_API_KEY")
self.eleven_labs_api_key = os.environ.get("ELEVEN_LABS_API_KEY")
self.client = ElevenLabs(api_key=self.eleven_labs_api_key)
self.aai = aai.settings.api_key = self.assembly_ai_api_key
def invoke(self, content_data):
"""Invokes the content production process.
Args:
content_data (list): A list of ContentData objects containing script and montage URL.
Raises:
ContentError: An error occurred during the content production process.
"""
try:
self._create_voice_over(content_data)
self._create_video_short(content_data)
except Exception as e:
raise ContentError("Error creating content: " + str(e))
return
def _create_voice_over(self, content_data):
"""Creates voice-over text-to-speech (TTS) audio files and subtitles.
Args:
content_data (list): A list of ContentData objects containing script and montage URL.
"""
for i, data in enumerate(content_data):
if data.category == "tech_science":
script = f"What's going on in tech today?\n\n" + data.script
else:
script = f"What's going on in {data.category} today?\n\n" + data.script
audio_file_name = f"audio_clip_{i}.mp3"
audio = self.client.generate(
text=script,
voice=Voice(
voice_id="yeNojDSkqjMLki231UWs",
settings=VoiceSettings(stability=0.5, similarity_boost=0.75),
),
model="eleven_turbo_v2",
)
elevenlabs.save(audio, audio_file_name)
transcript = aai.Transcriber().transcribe(audio_file_name)
print("Transcript: ", transcript.text)
subtitles = transcript.export_subtitles_srt(chars_per_caption=20)
with open(f"subtitles_{i}.srt", "w") as file:
file.write(subtitles)
def _create_video_short(self, content_data):
"""Creates short video clips with text-to-speech (TTS) audio and subtitles.
Args:
content_data (list): A list of ContentData objects containing script and montage URL.
"""
for i, (data) in enumerate(content_data):
montage_url = data.montage_url
filename = f"audio_clip_{i}.mp3"
narration_audio = AudioFileClip(filename)
audio_duration = narration_audio.duration
def generator(txt, duration):
"""Generates a video clip with centered text over a clip mathcing the topic category.
Args:
txt (str): The subtitle text.
duration (float): Duration of the subtitle text display.
Returns:
CompositeVideoClip: A video clip with the subtitle text and background.
"""
font_size = 40
dummy_clip = TextClip(
txt, font="Arial-Bold", fontsize=font_size
) # Create a dummy TextClip to measure text size
text_width, text_height = dummy_clip.size
padding_x = 20
padding_y = 10
bg_width = text_width + padding_x * 2
bg_height = text_height + padding_y * 2
return CompositeVideoClip(
[
ColorClip((bg_width, bg_height), color=(0, 0, 0))
.set_opacity(0.5)
.set_duration(duration),
TextClip(
txt,
font="Arial-Bold",
fontsize=font_size,
color="white",
align="center",
size=(
bg_width,
None,
), # Set width, let height adjust dynamically
)
.set_position(("center", "center"))
.set_duration(duration),
]
)
subtitles_clip = SubtitlesClip(
f"subtitles_{i}.srt",
lambda txt, duration=audio_duration: generator(txt, duration),
)
video_clip = VideoFileClip(montage_url)
video_duration = video_clip.duration
if video_duration < audio_duration:
repeated_video_clip = video_clip.loop(duration=audio_duration)
adjusted_video_clip = repeated_video_clip
elif video_duration > audio_duration:
adjusted_video_clip = video_clip.subclip(0, audio_duration)
final_with_subtitles = adjusted_video_clip.set_audio(
narration_audio
).set_duration(adjusted_video_clip.duration)
final_with_subtitles = CompositeVideoClip(
[final_with_subtitles, subtitles_clip.set_pos("center")]
)
# outro_audio = AudioFileClip("outro_audio.mp3")
# outro_clip = VideoFileClip("outro_clip.mp4").set_audio(outro_audio)
# clip_array = [final_with_subtitles, outro_clip]
# final_short = concatenate_videoclips(clip_array)
final_short = final_with_subtitles
final_short.write_videofile(f"out/final_clip_{i}.mp4", fps=24)