lecyanh commited on
Commit
ca4a74b
·
verified ·
1 Parent(s): 8cf00e2

Upload 4 files

Browse files
Files changed (4) hide show
  1. src/app.py +55 -0
  2. src/packages.txt +1 -0
  3. src/processor.py +115 -0
  4. src/requirements.txt +6 -0
src/app.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import tempfile
3
+ import os
4
+ from processor import translate_and_dub
5
+
6
+ st.set_page_config(page_title="Video Translator", page_icon="📺")
7
+
8
+ st.title("📺 Video Translator")
9
+
10
+ # --- SIDEBAR ---
11
+ with st.sidebar:
12
+ mode = st.radio("Chuyển ngữ", ('Tiếng Việt -> Trung Quốc', 'Trung Quốc -> Tiếng Việt'))
13
+ gender = st.radio("Giọng", ('Nữ', 'Nam'))
14
+ target_lang = "Chinese" if mode == 'Tiếng Việt -> Trung Quốc' else "Vietnamese"
15
+
16
+ # --- MAIN ---
17
+ uploaded_file = st.file_uploader("Upload Video", type=['mp4', 'mov'])
18
+
19
+ if uploaded_file is not None:
20
+ # Save Upload
21
+ tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
22
+ tfile.write(uploaded_file.read())
23
+ video_path = tfile.name
24
+
25
+ st.video(video_path) # Show original
26
+
27
+ if st.button('🚀 Dịch video'):
28
+ with st.spinner('Đang xử lý...'):
29
+ try:
30
+ # Get paths for Video and Subtitle file
31
+ dubbed_video, subtitle_file, segments = translate_and_dub(video_path, target_lang, gender)
32
+
33
+ st.success("Dịch thành công!")
34
+
35
+ st.subheader("Video đã dịch (Bật CC 💬 trong player)")
36
+
37
+ # THIS IS THE KEY CHANGE:
38
+ # We pass the video AND the subtitle file path
39
+ st.video(dubbed_video, subtitles=subtitle_file)
40
+
41
+ col1, col2 = st.columns(2)
42
+ with col1:
43
+ with open(dubbed_video, 'rb') as f:
44
+ st.download_button('⬇️ Tải Video', f, file_name="dubbed_video.mp4")
45
+ with col2:
46
+ with open(subtitle_file, 'rb') as f:
47
+ st.download_button('⬇️ Tải Subtitles', f, file_name="subtitles.vtt")
48
+
49
+ # Show text transcript below
50
+ with st.expander("Xem bản dịch"):
51
+ for seg in segments:
52
+ st.write(f"**{seg['start']}s**: {seg['text']}")
53
+
54
+ except Exception as e:
55
+ st.error(f"Error: {e}")
src/packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
src/processor.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import asyncio
3
+ import textwrap
4
+ import numpy as np
5
+ from PIL import Image, ImageDraw, ImageFont
6
+ import google.generativeai as genai
7
+ import edge_tts
8
+ from moviepy import VideoFileClip, AudioFileClip, ImageClip, CompositeVideoClip
9
+ from dotenv import load_dotenv
10
+ import os
11
+
12
+ # Configure Gemini
13
+ load_dotenv()
14
+ GENAI_API_KEY = os.getenv("GENAI_API_KEY")
15
+ genai.configure(api_key=GENAI_API_KEY)
16
+
17
+ def format_timestamp(seconds):
18
+ """Converts seconds (float) to WebVTT format (HH:MM:SS.mmm)"""
19
+ milliseconds = int((seconds % 1) * 1000)
20
+ minutes = int(seconds // 60)
21
+ hours = int(minutes // 60)
22
+ minutes = minutes % 60
23
+ seconds = int(seconds % 60)
24
+ return f"{hours:02}:{minutes:02}:{seconds:02}.{milliseconds:03}"
25
+
26
+ async def generate_dubbing(text, voice, output_file):
27
+ communicate = edge_tts.Communicate(text, voice)
28
+ await communicate.save(output_file)
29
+
30
+ def translate_and_dub(video_path, target_lang, gender="Female"):
31
+ base_name = os.path.splitext(video_path)[0]
32
+ audio_path = f"{base_name}_temp.mp3"
33
+ dub_audio_path = f"{base_name}_dub.mp3"
34
+ output_video_path = f"{base_name}_dubbed.mp4"
35
+ output_sub_path = f"{base_name}_subs.vtt" # We create a VTT file now
36
+
37
+ # 1. EXTRACT AUDIO & INFO
38
+ with VideoFileClip(video_path) as video:
39
+ video.audio.write_audiofile(audio_path, logger=None)
40
+ duration = video.duration
41
+
42
+ # 2. AI TRANSLATION & TIMESTAMPS
43
+ model = genai.GenerativeModel("gemini-2.5-flash")
44
+
45
+ if target_lang == "Chinese":
46
+ voice = "zh-CN-YunxiNeural" if gender == "Male" else "zh-CN-XiaoxiaoNeural"
47
+ lang_prompt = "Simplified Chinese"
48
+ else:
49
+ voice = "vi-VN-NamMinhNeural" if gender == "Male" else "vi-VN-HoaiMyNeural"
50
+ lang_prompt = "Vietnamese"
51
+
52
+ prompt = f"""
53
+ Listen to this audio. Return a JSON list of segments.
54
+ For each segment, translate the spoken content into {lang_prompt}.
55
+ Format:
56
+ [
57
+ {{"start": 0.0, "end": 2.5, "text": "Translated text here"}},
58
+ {{"start": 2.5, "end": 5.0, "text": "Next text here"}}
59
+ ]
60
+ Use seconds for timestamps.
61
+ Ensure the segments cover the whole video.
62
+ """
63
+
64
+ print("Sending to Gemini...")
65
+ audio_file = genai.upload_file(path=audio_path)
66
+ response = model.generate_content([prompt, audio_file], generation_config={"response_mime_type": "application/json"})
67
+
68
+ try:
69
+ segments = json.loads(response.text)
70
+ except json.JSONDecodeError:
71
+ segments = [{"start": 0, "end": duration, "text": "Translation Error: Could not parse JSON."}]
72
+
73
+ # 3. GENERATE VTT FILE (SUBTITLES)
74
+ print("Creating subtitles...")
75
+ vtt_content = "WEBVTT\n\n"
76
+ full_text_for_dub = []
77
+
78
+ for seg in segments:
79
+ start_time = format_timestamp(float(seg['start']))
80
+ end_time = format_timestamp(float(seg['end']))
81
+ text = seg['text']
82
+
83
+ # Add to VTT
84
+ vtt_content += f"{start_time} --> {end_time}\n{text}\n\n"
85
+
86
+ # Collect text for dubbing
87
+ full_text_for_dub.append(text)
88
+
89
+ # Save VTT file
90
+ with open(output_sub_path, "w", encoding="utf-8") as f:
91
+ f.write(vtt_content)
92
+
93
+ # 4. GENERATE DUBBING AUDIO
94
+ print("Generating voice...")
95
+ full_text = " ".join(full_text_for_dub)
96
+ loop = asyncio.new_event_loop()
97
+ asyncio.set_event_loop(loop)
98
+ loop.run_until_complete(generate_dubbing(full_text, voice, dub_audio_path))
99
+
100
+ # 5. MERGE AUDIO ONLY (No video re-encoding needed usually, but MoviePy is safest)
101
+ print("Merging new audio...")
102
+ with VideoFileClip(video_path) as video:
103
+ with AudioFileClip(dub_audio_path) as dub:
104
+ # Handle duration mismatch
105
+ if dub.duration > video.duration:
106
+ dub = dub.subclipped(0, video.duration)
107
+
108
+ final_clip = video.with_audio(dub)
109
+ final_clip.write_videofile(output_video_path, codec="libx264", audio_codec="aac", logger=None)
110
+
111
+ # Cleanup temp files
112
+ if os.path.exists(audio_path): os.remove(audio_path)
113
+ # Keeping dub audio and vtt might be useful for the user
114
+
115
+ return output_video_path, output_sub_path, segments
src/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ deep-translator
3
+ edge-tts
4
+ google-generativeai
5
+ moviepy
6
+ nest_asyncio