Upload 8 files
Browse files- README.md +9 -12
- app.py +114 -0
- audio_processing.py +38 -0
- config.py +6 -0
- content_generation.py +122 -0
- requirements.txt +9 -0
- utils.py +101 -0
- video_processing.py +157 -0
README.md
CHANGED
@@ -1,12 +1,9 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
---
|
11 |
-
|
12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
# Ứng dụng Tạo Nội dung và Video
|
2 |
+
|
3 |
+
Ứng dụng này cho phép bạn tạo nội dung và video tự động từ văn bản đầu vào.
|
4 |
+
|
5 |
+
## Cài đặt
|
6 |
+
|
7 |
+
1. Clone repository này:
|
8 |
+
```bash
|
9 |
+
git clone <repository_url>
|
|
|
|
|
|
app.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import mimetypes
|
3 |
+
import os
|
4 |
+
|
5 |
+
import gradio as gr
|
6 |
+
|
7 |
+
from audio_processing import async_text_to_speech, text_to_speech
|
8 |
+
from content_generation import create_content, CONTENT_TYPES
|
9 |
+
from video_processing import create_video_func
|
10 |
+
|
11 |
+
# Cấu hình API keys (di chuyển sang config.py)
|
12 |
+
# GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
13 |
+
# OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
14 |
+
|
15 |
+
# Danh sách giọng đọc
|
16 |
+
VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
17 |
+
|
18 |
+
# Danh sách ngôn ngữ (chưa được sử dụng trong mã)
|
19 |
+
LANGUAGES = ["Tiếng Anh", "Tiếng Việt", "Tiếng Hindi"]
|
20 |
+
|
21 |
+
# ... (CONTENT_TYPES, CONTENT_TYPE_INSTRUCTIONS)
|
22 |
+
# Danh sách loại nội dung và hướng dẫn mặc định cho từng loại
|
23 |
+
CONTENT_TYPES = ["podcast", "giới thiệu", "triết lý sống", "Phổ biến kiến thức thống kê"]
|
24 |
+
CONTENT_TYPE_INSTRUCTIONS = {
|
25 |
+
"podcast": """
|
26 |
+
Tone giọng: Gần gũi, thân thiện nhưng chuyên sâu, thể hiện sự am hiểu về chủ đề.
|
27 |
+
Cấu trúc:
|
28 |
+
- Bắt đầu bằng một câu hỏi kích thích tư duy hoặc một câu chuyện mở màn gây tò mò.
|
29 |
+
- Triển khai các luận điểm theo từng bước. Sử dụng câu từ mạnh mẽ, ví dụ điển hình hoặc những câu nói nổi tiếng.
|
30 |
+
- Xây dựng các phần chuyển tiếp mượt mà giữa các ý.
|
31 |
+
- Kết thúc podcast với một thông điệp sâu sắc, để lại sự suy ngẫm cho thính giả.
|
32 |
+
Mục tiêu: Mang lại kiến thức giá trị, lôi cuốn thính giả tham gia suy nghĩ và cảm nhận sâu sắc về chủ đề.
|
33 |
+
""",
|
34 |
+
"giới thiệu": """
|
35 |
+
Tone giọng: Chuyên nghiệp, gãy gọn nhưng vẫn có sự truyền cảm.
|
36 |
+
Cấu trúc:
|
37 |
+
- Bắt đầu với một câu khẳng định mạnh mẽ về đối tượng được giới thiệu.
|
38 |
+
- Giải thích mục tiêu của phần giới thiệu, nhấn mạnh tầm quan trọng hoặc sự khác biệt.
|
39 |
+
- Kết thúc với một lời kêu gọi hành động, khích lệ người nghe tiếp tục lắng nghe hoặc tham gia.
|
40 |
+
Mục tiêu: Đưa ra thông tin cô đọng, hấp dẫn, khiến người nghe cảm thấy bị thu hút và muốn tìm hiểu thêm.
|
41 |
+
""",
|
42 |
+
"triết lý sống": """
|
43 |
+
Tone giọng: Sâu sắc, truyền cảm hứng, mang tính chiêm nghiệm.
|
44 |
+
Cấu trúc:
|
45 |
+
- Bắt đầu bằng một câu hỏi sâu sắc hoặc ẩn dụ về cuộc sống.
|
46 |
+
- Triển khai các luận điểm chặt chẽ, xen lẫn cảm xúc và những ví dụ đời thực hoặc những câu nói triết lý.
|
47 |
+
- Kết thúc với một thông điệp sâu sắc, khơi dậy suy ngẫm cho người nghe.
|
48 |
+
Mục tiêu: Khơi gợi suy nghĩ sâu sắc về cuộc sống, khiến người nghe tìm thấy ý nghĩa hoặc giá trị trong câu chuyện.
|
49 |
+
""",
|
50 |
+
"Phổ biến kiến thức Thống kê": """
|
51 |
+
Tone giọng: Thân thiện, dễ hiểu, và mang tính giáo dục.
|
52 |
+
Cấu trúc:
|
53 |
+
- Bắt đầu với một câu hỏi hoặc một tình huống thực tế để thu hút sự chú ý.
|
54 |
+
- Giải thích các khái niệm thống kê cơ bản một cách đơn giản và dễ hiểu, sử dụng ví dụ thực tế để minh họa.
|
55 |
+
- Đưa ra các ứng dụng thực tế của thống kê trong đời sống hàng ngày hoặc trong các lĩnh vực cụ thể.
|
56 |
+
- Kết thúc với một thông điệp khuyến khích người nghe áp dụng kiến thức thống kê vào cuộc sống.
|
57 |
+
Mục tiêu: Giúp người nghe hiểu và yêu thích thống kê, thấy được giá trị và ứng dụng của nó trong cuộc sống.
|
58 |
+
"""
|
59 |
+
}
|
60 |
+
|
61 |
+
def create_docx(content, output_path):
|
62 |
+
"""
|
63 |
+
Tạo file docx từ nội dung.
|
64 |
+
"""
|
65 |
+
doc = Document()
|
66 |
+
doc.add_paragraph(content)
|
67 |
+
doc.save(output_path)
|
68 |
+
|
69 |
+
def process_pdf(file_path):
|
70 |
+
"""
|
71 |
+
Xử lý file PDF và trích xuất nội dung.
|
72 |
+
"""
|
73 |
+
doc = fitz.open(file_path)
|
74 |
+
text = ""
|
75 |
+
for page in doc:
|
76 |
+
text += page.get_text()
|
77 |
+
return text
|
78 |
+
|
79 |
+
def process_docx(file_path):
|
80 |
+
"""
|
81 |
+
Xử lý file DOCX và trích xuất nội dung.
|
82 |
+
"""
|
83 |
+
doc = Document(file_path)
|
84 |
+
text = ""
|
85 |
+
for para in doc.paragraphs:
|
86 |
+
text += para.text
|
87 |
+
return text
|
88 |
+
|
89 |
+
# Giao diện Gradio
|
90 |
+
def interface():
|
91 |
+
with gr.Blocks() as app:
|
92 |
+
# ... (Các tab khác)
|
93 |
+
|
94 |
+
with gr.Tab("Tạo Âm thanh"):
|
95 |
+
text_input = gr.Textbox(label="Nhập văn bản để chuyển đổi")
|
96 |
+
voice_select = gr.Dropdown(label="Chọn giọng đọc", choices=VOICES) # Dropdown cho voice_select
|
97 |
+
audio_button = gr.Button("Tạo Âm thanh")
|
98 |
+
audio_output = gr.Audio(label="Âm thanh tạo ra")
|
99 |
+
download_audio = gr.File(label="Tải xuống file âm thanh", interactive=False)
|
100 |
+
|
101 |
+
def text_to_speech_func(text, voice):
|
102 |
+
audio_path = text_to_speech(text, voice, "Tiếng Việt")
|
103 |
+
return audio_path, audio_path
|
104 |
+
|
105 |
+
audio_button.click(text_to_speech_func, inputs=[text_input, voice_select], outputs=[audio_output, download_audio])
|
106 |
+
|
107 |
+
# ... (Các tab khác)
|
108 |
+
|
109 |
+
return app
|
110 |
+
|
111 |
+
# Khởi chạy ứng dụng
|
112 |
+
if __name__ == "__main__":
|
113 |
+
app = interface()
|
114 |
+
app.launch()
|
audio_processing.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# audio_processing.py
|
2 |
+
import asyncio
|
3 |
+
import os
|
4 |
+
import tempfile
|
5 |
+
|
6 |
+
from openai import OpenAI
|
7 |
+
|
8 |
+
# Lấy API key từ biến môi trường
|
9 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
10 |
+
|
11 |
+
# Khởi tạo client OpenAI
|
12 |
+
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
13 |
+
|
14 |
+
def text_to_speech(text, voice, language):
|
15 |
+
"""
|
16 |
+
Chuyển đổi văn bản thành giọng nói bằng OpenAI API.
|
17 |
+
"""
|
18 |
+
try:
|
19 |
+
response = openai_client.audio.speech.create(
|
20 |
+
model="tts-1",
|
21 |
+
voice=voice,
|
22 |
+
input=text
|
23 |
+
)
|
24 |
+
|
25 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_audio_file:
|
26 |
+
for chunk in response.iter_bytes(chunk_size=4096):
|
27 |
+
temp_audio_file.write(chunk)
|
28 |
+
|
29 |
+
return temp_audio_file.name
|
30 |
+
except Exception as e:
|
31 |
+
return f"Lỗi khi chuyển đổi văn bản thành giọng nói: {str(e)}"
|
32 |
+
|
33 |
+
async def async_text_to_speech(text, voice, language):
|
34 |
+
"""
|
35 |
+
Chuyển đổi văn bản thành giọng nói (bất đồng bộ).
|
36 |
+
"""
|
37 |
+
loop = asyncio.get_event_loop()
|
38 |
+
return await loop.run_in_executor(None, text_to_speech, text, voice, language)
|
config.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
# Cấu hình API keys
|
4 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
5 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
6 |
+
PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
|
content_generation.py
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# content_generation.py
|
2 |
+
from groq import Groq
|
3 |
+
from openai import OpenAI
|
4 |
+
import os
|
5 |
+
|
6 |
+
# Lấy API key từ biến môi trường
|
7 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
8 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
9 |
+
|
10 |
+
# Khởi tạo client OpenAI
|
11 |
+
openai_client = OpenAI(api_key=OPENAI_API_KEY)
|
12 |
+
|
13 |
+
# Danh sách loại nội dung và hướng dẫn mặc định cho từng loại
|
14 |
+
CONTENT_TYPES = ["podcast", "giới thiệu", "triết lý sống", "Phổ biến kiến thức thống kê"]
|
15 |
+
CONTENT_TYPE_INSTRUCTIONS = {
|
16 |
+
"podcast": """
|
17 |
+
Tone giọng: Gần gũi, thân thiện nhưng chuyên sâu, thể hiện sự am hiểu về chủ đề.
|
18 |
+
Cấu trúc:
|
19 |
+
- Bắt đầu bằng một câu hỏi kích thích tư duy hoặc một câu chuyện mở màn gây tò mò.
|
20 |
+
- Triển khai các luận điểm theo từng bước. Sử dụng câu từ mạnh mẽ, ví dụ điển hình hoặc những câu nói nổi tiếng.
|
21 |
+
- Xây dựng các phần chuyển tiếp mượt mà giữa các ý.
|
22 |
+
- Kết thúc podcast với một thông điệp sâu sắc, để lại sự suy ngẫm cho thính giả.
|
23 |
+
Mục tiêu: Mang lại kiến thức giá trị, lôi cuốn thính giả tham gia suy nghĩ và cảm nhận sâu sắc về chủ đề.
|
24 |
+
""",
|
25 |
+
"giới thiệu": """
|
26 |
+
Tone giọng: Chuyên nghiệp, gãy gọn nhưng vẫn có sự truyền cảm.
|
27 |
+
Cấu trúc:
|
28 |
+
- Bắt đầu với một câu khẳng định mạnh mẽ về đối tượng được giới thiệu.
|
29 |
+
- Giải thích mục tiêu của phần giới thiệu, nhấn mạnh tầm quan trọng hoặc sự khác biệt.
|
30 |
+
- Kết thúc với một lời kêu gọi hành động, khích lệ người nghe tiếp tục lắng nghe hoặc tham gia.
|
31 |
+
Mục tiêu: Đưa ra thông tin cô đọng, hấp dẫn, khiến người nghe cảm thấy bị thu hút và muốn tìm hiểu thêm.
|
32 |
+
""",
|
33 |
+
"triết lý sống": """
|
34 |
+
Tone giọng: Sâu sắc, truyền cảm hứng, mang tính chiêm nghiệm.
|
35 |
+
Cấu trúc:
|
36 |
+
- Bắt đầu bằng một câu hỏi sâu sắc hoặc ẩn dụ về cuộc sống.
|
37 |
+
- Triển khai các luận điểm chặt chẽ, xen lẫn cảm xúc và những ví dụ đời thực hoặc những câu nói triết lý.
|
38 |
+
- Kết thúc với một thông điệp sâu sắc, khơi dậy suy ngẫm cho người nghe.
|
39 |
+
Mục tiêu: Khơi gợi suy nghĩ sâu sắc về cuộc sống, khiến người nghe tìm thấy ý nghĩa hoặc giá trị trong câu chuyện.
|
40 |
+
""",
|
41 |
+
"Phổ biến kiến thức Thống kê": """
|
42 |
+
Tone giọng: Thân thiện, dễ hiểu, và mang tính giáo dục.
|
43 |
+
Cấu trúc:
|
44 |
+
- Bắt đầu với một câu hỏi hoặc một tình huống thực tế để thu hút sự chú ý.
|
45 |
+
- Giải thích các khái niệm thống kê cơ bản một cách đơn giản và dễ hiểu, sử dụng ví dụ thực tế để minh họa.
|
46 |
+
- Đưa ra các ứng dụng thực tế của thống kê trong đời sống hàng ngày hoặc trong các lĩnh vực cụ thể.
|
47 |
+
- Kết thúc với một thông điệp khuyến khích người nghe áp dụng kiến thức thống kê vào cuộc sống.
|
48 |
+
Mục tiêu: Giúp người nghe hiểu và yêu thích thống kê, thấy được giá trị và ứng dụng của nó trong cuộc sống.
|
49 |
+
"""
|
50 |
+
}
|
51 |
+
|
52 |
+
def create_content(prompt, content_type, language):
|
53 |
+
"""
|
54 |
+
Tạo nội dung dựa trên prompt, loại nội dung và ngôn ngữ.
|
55 |
+
"""
|
56 |
+
content_type_instructions = CONTENT_TYPE_INSTRUCTIONS.get(content_type, "")
|
57 |
+
general_instructions = f"""
|
58 |
+
Viết một kịch bản dựa trên các ý chính và ý tưởng sáng tạo từ yêu cầu của người dùng. Sử dụng giọng điệu trò chuyện và bao gồm bất kỳ bối cảnh hoặc giải thích cần thiết nào để làm cho nội dung dễ tiếp cận với khán giả.
|
59 |
+
Bắt đầu kịch bản bằng cách nêu rõ đây là một bài tóm tắt, tham chiếu đến tiêu đề hoặc đề mục trong văn bản đầu vào. Nếu văn bản đầu vào không có tiêu đề, hãy đưa ra một tóm tắt ngắn gọn về nội dung được đề cập để mở đầu.
|
60 |
+
Bao gồm các định nghĩa và thuật ngữ rõ ràng, cùng với ví dụ cho tất cả các vấn đề chính.
|
61 |
+
Không bao gồm bất kỳ placeholder nào trong ngoặc vuông như [Host] hoặc [Guest]. Thiết kế đầu ra của bạn để được đọc to - nó sẽ được chuyển đổi trực tiếp thành âm thanh.
|
62 |
+
Chỉ có một người nói, bạn. Giữ đúng chủ đề và duy trì một luồng hấp dẫn.
|
63 |
+
Tóm tắt một cách tự nhiên những hiểu biết và bài học chính từ bài tóm tắt. Điều này nên diễn ra một cách tự nhiên từ cuộc trò chuyện, nhắc lại các điểm chính một cách thân mật, như trong một cuộc trò chuyện.
|
64 |
+
Bài tóm tắt nên có khoảng 3000 từ.
|
65 |
+
Hãy tuân theo những hướng dẫn cụ thể sau cho thể loại {content_type}:
|
66 |
+
{content_type_instructions}
|
67 |
+
Ngôn ngữ sử dụng: {language}
|
68 |
+
"""
|
69 |
+
|
70 |
+
try:
|
71 |
+
client = Groq(api_key=GROQ_API_KEY)
|
72 |
+
chat_completion = client.chat.completions.create(
|
73 |
+
model="mixtral-8x7b-32768",
|
74 |
+
messages=[
|
75 |
+
{"role": "system", "content": general_instructions},
|
76 |
+
{"role": "user", "content": prompt}
|
77 |
+
],
|
78 |
+
temperature=0.7,
|
79 |
+
max_tokens=8000
|
80 |
+
)
|
81 |
+
return chat_completion.choices[0].message.content
|
82 |
+
except Exception as e:
|
83 |
+
return f"Lỗi khi tạo nội dung: {str(e)}"
|
84 |
+
|
85 |
+
def extract_key_contents(script, num_contents=30):
|
86 |
+
"""
|
87 |
+
Trích xuất các ý chính từ script.
|
88 |
+
"""
|
89 |
+
try:
|
90 |
+
response = openai_client.chat.completions.create(
|
91 |
+
model="gpt-3.5-turbo",
|
92 |
+
messages=[
|
93 |
+
{"role": "system", "content": f"Bạn là một chuyên gia phân tích nội dung. Hãy trích xuất chính xác {num_contents} ý chính quan trọng nhất từ đoạn văn sau, mỗi ý không quá 20 từ."},
|
94 |
+
{"role": "user", "content": script}
|
95 |
+
]
|
96 |
+
)
|
97 |
+
|
98 |
+
# In response để kiểm tra
|
99 |
+
print("Response từ OpenAI:", response)
|
100 |
+
|
101 |
+
key_contents = response.choices[0].message.content.split('\n')
|
102 |
+
return key_contents[:num_contents]
|
103 |
+
except Exception as e:
|
104 |
+
print(f"Lỗi khi trích xuất nội dung: {str(e)}")
|
105 |
+
return []
|
106 |
+
|
107 |
+
def generate_image_prompt(content):
|
108 |
+
"""
|
109 |
+
Tạo prompt cho hình ảnh từ nội dung.
|
110 |
+
"""
|
111 |
+
try:
|
112 |
+
response = openai_client.chat.completions.create(
|
113 |
+
model="gpt-3.5-turbo",
|
114 |
+
messages=[
|
115 |
+
{"role": "system", "content": "You are an expert at creating prompts for AI image generation. Create a short, concise prompt in English to visually describe the following content. The content may be in Vietnamese, but your prompt should always be in English."},
|
116 |
+
{"role": "user", "content": content}
|
117 |
+
]
|
118 |
+
)
|
119 |
+
return response.choices[0].message.content.strip()
|
120 |
+
except Exception as e:
|
121 |
+
print(f"Lỗi khi tạo prompt cho hình ảnh: {str(e)}")
|
122 |
+
return f"A visual representation of: {content}" # Fallback prompt nếu có lỗi
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
groq
|
3 |
+
openai
|
4 |
+
python-docx
|
5 |
+
PyMuPDF
|
6 |
+
Pillow
|
7 |
+
sentence-transformers
|
8 |
+
moviepy
|
9 |
+
loguru
|
utils.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import glob
|
2 |
+
import os
|
3 |
+
import random
|
4 |
+
|
5 |
+
import requests
|
6 |
+
from loguru import logger
|
7 |
+
from moviepy.editor import AudioFileClip, VideoFileClip, concatenate_videoclips
|
8 |
+
|
9 |
+
def get_pexels_image(query):
|
10 |
+
"""
|
11 |
+
Lấy ảnh từ Pexels API dựa trên query.
|
12 |
+
"""
|
13 |
+
api_key = os.getenv('Pexels_API_KEY')
|
14 |
+
# Thêm từ khóa "Vietnamese" vào truy vấn và tăng số lượng ảnh lên 30
|
15 |
+
url = f"https://api.pexels.com/v1/search?query={query}%20Vietnamese&per_page=30"
|
16 |
+
headers = {"Authorization": api_key}
|
17 |
+
response = requests.get(url, headers=headers)
|
18 |
+
if response.status_code == 200:
|
19 |
+
data = response.json()
|
20 |
+
if data['photos']:
|
21 |
+
# Chọn ngẫu nhiên một ảnh từ kết quả
|
22 |
+
return random.choice(data['photos'])['src']['medium']
|
23 |
+
return None
|
24 |
+
|
25 |
+
def get_bgm_file(bgm_type: str = "random", bgm_file: str = ""):
|
26 |
+
"""
|
27 |
+
Lấy file nhạc nền.
|
28 |
+
"""
|
29 |
+
if not bgm_type:
|
30 |
+
return ""
|
31 |
+
if bgm_type == "random":
|
32 |
+
suffix = "*.mp3"
|
33 |
+
song_dir = utils.song_dir() # Đảm bảo utils.song_dir() trả về đường dẫn đúng
|
34 |
+
files = glob.glob(os.path.join(song_dir, suffix))
|
35 |
+
return random.choice(files) if files else ""
|
36 |
+
|
37 |
+
if os.path.exists(bgm_file):
|
38 |
+
return bgm_file
|
39 |
+
|
40 |
+
return ""
|
41 |
+
|
42 |
+
def combine_videos(combined_video_path: str,
|
43 |
+
video_paths: list[str],
|
44 |
+
audio_file: str,
|
45 |
+
max_clip_duration: int = 5,
|
46 |
+
threads: int = 2,
|
47 |
+
) -> str:
|
48 |
+
"""
|
49 |
+
Kết hợp nhiều video thành một video duy nhất.
|
50 |
+
"""
|
51 |
+
audio_clip = AudioFileClip(audio_file)
|
52 |
+
audio_duration = audio_clip.duration
|
53 |
+
logger.info(f"Max duration of audio: {audio_duration} seconds")
|
54 |
+
|
55 |
+
clips = []
|
56 |
+
video_duration = 0
|
57 |
+
|
58 |
+
while video_duration < audio_duration:
|
59 |
+
random.shuffle(video_paths)
|
60 |
+
|
61 |
+
for video_path in video_paths:
|
62 |
+
clip = VideoFileClip(video_path).without_audio()
|
63 |
+
if (audio_duration - video_duration) < clip.duration:
|
64 |
+
clip = clip.subclip(0, (audio_duration - video_duration))
|
65 |
+
elif max_clip_duration < clip.duration:
|
66 |
+
clip = clip.subclip(0, max_clip_duration)
|
67 |
+
clip = clip.set_fps(30)
|
68 |
+
clips.append(clip)
|
69 |
+
video_duration += clip.duration
|
70 |
+
|
71 |
+
final_clip = concatenate_videoclips(clips)
|
72 |
+
final_clip = final_clip.set_fps(30)
|
73 |
+
logger.info(f"Writing combined video to {combined_video_path}")
|
74 |
+
final_clip.write_videofile(combined_video_path, threads=threads)
|
75 |
+
logger.success(f"Completed combining videos")
|
76 |
+
return combined_video_path
|
77 |
+
|
78 |
+
def generate_video(video_paths: list[str],
|
79 |
+
audio_path: str,
|
80 |
+
output_file: str,
|
81 |
+
) -> str:
|
82 |
+
"""
|
83 |
+
Tạo video cuối cùng bằng cách kết hợp video và âm thanh.
|
84 |
+
"""
|
85 |
+
logger.info(f"Start generating video")
|
86 |
+
combined_video_path = "temp_combined_video.mp4"
|
87 |
+
|
88 |
+
combine_videos(combined_video_path, video_paths, audio_path)
|
89 |
+
|
90 |
+
# Add audio to the final video
|
91 |
+
final_video = VideoFileClip(combined_video_path)
|
92 |
+
audio_clip = AudioFileClip(audio_path)
|
93 |
+
final_video = final_video.set_audio(audio_clip)
|
94 |
+
|
95 |
+
logger.info(f"Writing final video to {output_file}")
|
96 |
+
final_video.write_videofile(output_file, audio_codec="aac")
|
97 |
+
|
98 |
+
# Remove temporary video
|
99 |
+
os.remove(combined_video_path)
|
100 |
+
logger.success(f"Completed generating video: {output_file}")
|
101 |
+
return output_file
|
video_processing.py
ADDED
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import random
|
3 |
+
import shutil
|
4 |
+
import tempfile
|
5 |
+
from concurrent.futures import ThreadPoolExecutor
|
6 |
+
from moviepy.editor import (
|
7 |
+
AudioFileClip,
|
8 |
+
CompositeVideoClip,
|
9 |
+
ImageClip,
|
10 |
+
VideoFileClip,
|
11 |
+
concatenate_videoclips,
|
12 |
+
vfx,
|
13 |
+
)
|
14 |
+
from moviepy.video.tools.subtitles import SubtitlesClip
|
15 |
+
import tqdm
|
16 |
+
|
17 |
+
from sentence_transformers import SentenceTransformer, util
|
18 |
+
|
19 |
+
# Khởi tạo model sentence transformer
|
20 |
+
model = SentenceTransformer('all-MiniLM-L6-v2')
|
21 |
+
|
22 |
+
# Tăng số lượng ảnh lên 30
|
23 |
+
NUM_IMAGES = 30
|
24 |
+
|
25 |
+
def add_transitions(clips, transition_duration=1):
|
26 |
+
"""
|
27 |
+
Thêm hiệu ứng chuyển cảnh giữa các clip.
|
28 |
+
"""
|
29 |
+
final_clips = []
|
30 |
+
for i, clip in enumerate(clips):
|
31 |
+
start_time = i * (clip.duration - transition_duration)
|
32 |
+
end_time = start_time + clip.duration
|
33 |
+
|
34 |
+
if i > 0:
|
35 |
+
# Tạo hiệu ứng fade in
|
36 |
+
fade_in = clip.fx(vfx.fadeout, duration=transition_duration)
|
37 |
+
fade_in = fade_in.set_start(start_time)
|
38 |
+
final_clips.append(fade_in)
|
39 |
+
|
40 |
+
if i < len(clips) - 1:
|
41 |
+
# Tạo hiệu ứng fade out
|
42 |
+
fade_out = clip.fx(vfx.fadein, duration=transition_duration)
|
43 |
+
fade_out = fade_out.set_end(end_time)
|
44 |
+
final_clips.append(fade_out)
|
45 |
+
|
46 |
+
# Thêm clip gốc
|
47 |
+
final_clips.append(clip.set_start(start_time).set_end(end_time))
|
48 |
+
|
49 |
+
return CompositeVideoClip(final_clips)
|
50 |
+
|
51 |
+
def create_video(sentences, audio_files, video_files, output_path="output_video.mp4"):
|
52 |
+
"""
|
53 |
+
Tạo video từ các câu, file âm thanh và file video.
|
54 |
+
"""
|
55 |
+
clips = []
|
56 |
+
for sentence, audio_path, video_path in tqdm.tqdm(zip(sentences, audio_files, video_files), desc="Tạo video"):
|
57 |
+
audio = AudioFileClip(audio_path)
|
58 |
+
video = VideoFileClip(video_path).set_duration(audio.duration)
|
59 |
+
video = video.set_audio(audio)
|
60 |
+
clips.append(video)
|
61 |
+
|
62 |
+
final_video = concatenate_videoclips(clips, method="compose")
|
63 |
+
final_video.write_videofile(output_path, fps=24)
|
64 |
+
print(f"Đã tạo video: {output_path}")
|
65 |
+
return output_path
|
66 |
+
|
67 |
+
def process_images_parallel(image_patch, clip_duration):
|
68 |
+
"""
|
69 |
+
Xử lý song song các hình ảnh.
|
70 |
+
"""
|
71 |
+
with ThreadPoolExecutor() as executor:
|
72 |
+
futures = []
|
73 |
+
for content, image_path in image_patch:
|
74 |
+
if image_path:
|
75 |
+
future = executor.submit(ImageClip, image_path)
|
76 |
+
futures.append((future, clip_duration))
|
77 |
+
|
78 |
+
clips = []
|
79 |
+
for future, duration in futures:
|
80 |
+
clip = future.result().set_duration(duration)
|
81 |
+
clips.append(clip)
|
82 |
+
|
83 |
+
return clips
|
84 |
+
|
85 |
+
def process_script_for_video(script, dataset_path, use_dataset):
|
86 |
+
"""
|
87 |
+
Xử lý script để tạo video.
|
88 |
+
"""
|
89 |
+
sentences = extract_key_contents(script)
|
90 |
+
return sentences
|
91 |
+
|
92 |
+
def create_video_func(script, audio_path, dataset_path, use_dataset):
|
93 |
+
"""
|
94 |
+
Hàm chính để tạo video.
|
95 |
+
"""
|
96 |
+
sentences = process_script_for_video(script, dataset_path, use_dataset)
|
97 |
+
|
98 |
+
# Tạo thư mục tạm thời để lưu các file âm thanh tách biệt
|
99 |
+
temp_dir = tempfile.mkdtemp()
|
100 |
+
|
101 |
+
# Tách file âm thanh thành các đoạn nhỏ
|
102 |
+
audio_clips = split_audio(audio_path, len(sentences), temp_dir)
|
103 |
+
|
104 |
+
# Lấy đường dẫn của các video từ dataset
|
105 |
+
video_files = glob.glob(os.path.join(dataset_path, "*.mp4")) if use_dataset else []
|
106 |
+
|
107 |
+
# Đảm bảo số lượng câu, âm thanh và video là bằng nhau
|
108 |
+
min_length = min(len(sentences), len(audio_clips), len(video_files))
|
109 |
+
sentences = sentences[:min_length]
|
110 |
+
audio_clips = audio_clips[:min_length]
|
111 |
+
video_files = video_files[:min_length]
|
112 |
+
|
113 |
+
output_path = "output_video.mp4"
|
114 |
+
create_video(sentences, audio_clips, video_files, output_path)
|
115 |
+
|
116 |
+
# Xóa thư mục tạm thời
|
117 |
+
shutil.rmtree(temp_dir)
|
118 |
+
|
119 |
+
return output_path, output_path
|
120 |
+
|
121 |
+
def split_audio(audio_path, num_segments, output_dir):
|
122 |
+
"""
|
123 |
+
Chia file âm thanh thành các đoạn nhỏ.
|
124 |
+
"""
|
125 |
+
audio = AudioFileClip(audio_path)
|
126 |
+
duration = audio.duration
|
127 |
+
segment_duration = duration / num_segments
|
128 |
+
|
129 |
+
audio_clips = []
|
130 |
+
for i in range(num_segments):
|
131 |
+
start = i * segment_duration
|
132 |
+
end = (i + 1) * segment_duration
|
133 |
+
segment = audio.subclip(start, end)
|
134 |
+
output_path = os.path.join(output_dir, f"segment_{i}.mp3")
|
135 |
+
segment.write_audiofile(output_path)
|
136 |
+
audio_clips.append(output_path)
|
137 |
+
|
138 |
+
return audio_clips
|
139 |
+
|
140 |
+
def find_matching_image(prompt, dataset_path, threshold=0.5):
|
141 |
+
"""
|
142 |
+
Tìm kiếm hình ảnh phù hợp với prompt trong dataset.
|
143 |
+
"""
|
144 |
+
prompt_embedding = model.encode(prompt, convert_to_tensor=True)
|
145 |
+
best_match = None
|
146 |
+
best_score = -1
|
147 |
+
|
148 |
+
for filename in os.listdir(dataset_path):
|
149 |
+
if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
|
150 |
+
image_path = os.path.join(dataset_path, filename)
|
151 |
+
image_name = os.path.splitext(filename)[0].replace('_', ' ')
|
152 |
+
image_embedding = model.encode(image_name, convert_to_tensor=True)
|
153 |
+
cosine_score = util.pytorch_cos_sim(prompt_embedding, image_embedding).item()
|
154 |
+
if cosine_score > best_score and cosine_score >= threshold:
|
155 |
+
best_score = cosine_score
|
156 |
+
best_match = image_path
|
157 |
+
return best_match
|