Commit ยท
7af5aaa
1
Parent(s): 8c64cc3
save fix
Browse files- init_defaults.py +9 -5
- routers/subtitle_generator.py +37 -13
- routers/video.py +12 -7
- tests/test_comprehensive_e2e.py +114 -82
init_defaults.py
CHANGED
|
@@ -26,7 +26,7 @@ defaults = {
|
|
| 26 |
"max_words_per_line": 3,
|
| 27 |
"uppercase": False,
|
| 28 |
"letter_spacing": 0.0,
|
| 29 |
-
"background_opacity":
|
| 30 |
"glow_intensity": 0,
|
| 31 |
"rotation_angle": 0.0,
|
| 32 |
"margin_h": 20
|
|
@@ -45,15 +45,17 @@ defaults = {
|
|
| 45 |
"margin_v": 120,
|
| 46 |
"pop_up_scale": 1.5,
|
| 47 |
"highlight_mode": "karaoke",
|
| 48 |
-
"back_box_enabled":
|
| 49 |
"display_mode": "sentence",
|
| 50 |
"max_words_per_line": 2,
|
| 51 |
"uppercase": True,
|
| 52 |
"letter_spacing": 1.0,
|
| 53 |
-
"background_opacity": 0.
|
| 54 |
"glow_intensity": 2,
|
| 55 |
"rotation_angle": -3.0,
|
| 56 |
-
"margin_h": 20
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
"professional": {
|
| 59 |
"name": "professional",
|
|
@@ -77,7 +79,9 @@ defaults = {
|
|
| 77 |
"background_opacity": 0.9,
|
| 78 |
"glow_intensity": 0,
|
| 79 |
"rotation_angle": 0.0,
|
| 80 |
-
"margin_h": 40
|
|
|
|
|
|
|
| 81 |
},
|
| 82 |
"storyteller": {
|
| 83 |
"name": "storyteller",
|
|
|
|
| 26 |
"max_words_per_line": 3,
|
| 27 |
"uppercase": False,
|
| 28 |
"letter_spacing": 0.0,
|
| 29 |
+
"background_opacity": 0.7,
|
| 30 |
"glow_intensity": 0,
|
| 31 |
"rotation_angle": 0.0,
|
| 32 |
"margin_h": 20
|
|
|
|
| 45 |
"margin_v": 120,
|
| 46 |
"pop_up_scale": 1.5,
|
| 47 |
"highlight_mode": "karaoke",
|
| 48 |
+
"back_box_enabled": True, # ุชูุนูู ุงูุจููุณ ููุฌูู
ูุฌ ุฃูุถุงู
|
| 49 |
"display_mode": "sentence",
|
| 50 |
"max_words_per_line": 2,
|
| 51 |
"uppercase": True,
|
| 52 |
"letter_spacing": 1.0,
|
| 53 |
+
"background_opacity": 0.6,
|
| 54 |
"glow_intensity": 2,
|
| 55 |
"rotation_angle": -3.0,
|
| 56 |
+
"margin_h": 20,
|
| 57 |
+
"box_highlight_type": "word",
|
| 58 |
+
"box_rounding": 15
|
| 59 |
},
|
| 60 |
"professional": {
|
| 61 |
"name": "professional",
|
|
|
|
| 79 |
"background_opacity": 0.9,
|
| 80 |
"glow_intensity": 0,
|
| 81 |
"rotation_angle": 0.0,
|
| 82 |
+
"margin_h": 40,
|
| 83 |
+
"box_highlight_type": "sentence",
|
| 84 |
+
"box_rounding": 5
|
| 85 |
},
|
| 86 |
"storyteller": {
|
| 87 |
"name": "storyteller",
|
routers/subtitle_generator.py
CHANGED
|
@@ -30,7 +30,10 @@ def generate_pro_ass(transcription, preset, output_path):
|
|
| 30 |
# ุชุญููู ุงูุดูุงููุฉ (0.0 - 1.0) ุฅูู ุตูุบุฉ ASS (00 - FF)
|
| 31 |
# ู
ูุงุญุธุฉ: ูู ASSุ ุงูู 00 ูู ู
ุนุชู
ุชู
ุงู
ุงู ู FF ูู ุดูุงู ุชู
ุงู
ุงู
|
| 32 |
back_alpha = int((1.0 - getattr(preset, 'background_opacity', 1.0)) * 255)
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
spacing = getattr(preset, 'letter_spacing', 0.0)
|
| 36 |
angle = getattr(preset, 'rotation_angle', 0.0)
|
|
@@ -45,7 +48,7 @@ ScaledBorderAndShadow: yes
|
|
| 45 |
[V4+ Styles]
|
| 46 |
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
| 47 |
Style: Main,{preset.font_name},{preset.font_size},{primary},{secondary},{outline},&H00000000,-1,0,0,0,100,100,{spacing},{angle},1,{preset.outline_width},{preset.shadow_depth},{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 48 |
-
Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H00000000,{back_color},-1,0,0,0,100,100,{spacing},{angle},3,10,0,{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 49 |
"""
|
| 50 |
|
| 51 |
events = "\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
|
@@ -126,38 +129,59 @@ Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H0
|
|
| 126 |
|
| 127 |
# ุฌุนู ุงููุต ุงูุฃุณุงุณู ุดูุงู ููููุงู (dimmed) ูุฅุฎูุงุก ุงูููู
ุฉ ุงููุดุทุฉ ุจู \alpha&HFF&
|
| 128 |
dim_alpha = "\\1a&H80&"
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 132 |
events += f"Dialogue: 0,{w_start},{w_end},BackBox,,0,0,0,,{base_display_text}\n"
|
| 133 |
else:
|
|
|
|
|
|
|
|
|
|
| 134 |
events += f"Dialogue: 0,{w_start},{w_end},Main,,0,0,0,,{base_display_text}\n"
|
| 135 |
|
| 136 |
-
# 2. ุทุจูุฉ ุงูููู
|
| 137 |
prefix_hl = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
|
| 138 |
suffix_hl = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
|
| 139 |
target_word_hl = words_list_base[i]
|
| 140 |
|
| 141 |
t1 = min(60, int(duration * 150))
|
| 142 |
t2 = t1 + 80
|
| 143 |
-
scale = int(preset.pop_up_scale * 100)
|
| 144 |
|
| 145 |
# ุฅุถุงูุฉ ุชุฃุซูุฑ ุงูุชููุฌ (Blur)
|
| 146 |
glow_val = getattr(preset, 'glow_intensity', 0)
|
| 147 |
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
|
| 148 |
|
| 149 |
-
# ุงูู Highlight ูููู ุจููู ุงูู secondary ูุจุฏูู ุดูุงููุฉ ู
ุน ุชุฃุซูุฑ Pop ูู
ููุงู
|
| 150 |
if angle != 0:
|
| 151 |
rotation = f"\\frz{angle}"
|
| 152 |
else:
|
| 153 |
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
with open(output_path, "w", encoding="utf-8-sig") as f:
|
| 163 |
f.write(header + events)
|
|
|
|
| 30 |
# ุชุญููู ุงูุดูุงููุฉ (0.0 - 1.0) ุฅูู ุตูุบุฉ ASS (00 - FF)
|
| 31 |
# ู
ูุงุญุธุฉ: ูู ASSุ ุงูู 00 ูู ู
ุนุชู
ุชู
ุงู
ุงู ู FF ูู ุดูุงู ุชู
ุงู
ุงู
|
| 32 |
back_alpha = int((1.0 - getattr(preset, 'background_opacity', 1.0)) * 255)
|
| 33 |
+
|
| 34 |
+
# ุงุณุชุฎุฏุงู
ููู ุงูุฎูููุฉ ู
ู ุงูู preset ุจุฏูุงู ู
ู ุงูุฃุณูุฏ ุงูุซุงุจุช
|
| 35 |
+
raw_back_color = rgb_to_ass(preset.back_color).replace("&H00", "")
|
| 36 |
+
back_color = f"&H{back_alpha:02X}{raw_back_color}"
|
| 37 |
|
| 38 |
spacing = getattr(preset, 'letter_spacing', 0.0)
|
| 39 |
angle = getattr(preset, 'rotation_angle', 0.0)
|
|
|
|
| 48 |
[V4+ Styles]
|
| 49 |
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
| 50 |
Style: Main,{preset.font_name},{preset.font_size},{primary},{secondary},{outline},&H00000000,-1,0,0,0,100,100,{spacing},{angle},1,{preset.outline_width},{preset.shadow_depth},{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 51 |
+
Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H00000000,{back_color},-1,0,0,0,100,100,{spacing},{angle},3,{getattr(preset, 'box_rounding', 10)},0,{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 52 |
"""
|
| 53 |
|
| 54 |
events = "\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
|
|
|
| 129 |
|
| 130 |
# ุฌุนู ุงููุต ุงูุฃุณุงุณู ุดูุงู ููููุงู (dimmed) ูุฅุฎูุงุก ุงูููู
ุฉ ุงููุดุทุฉ ุจู \alpha&HFF&
|
| 131 |
dim_alpha = "\\1a&H80&"
|
| 132 |
+
box_highlight_type = getattr(preset, 'box_highlight_type', 'word')
|
| 133 |
|
| 134 |
+
# ุฅุฐุง ูุงู ุงูููุน 'sentence'ุ ูุธูุฑ ุงูุตูุฏูู ุญูู ุงูุฌู
ูุฉ ูููุง ูู ุทุจูุฉ ุงูุฎูููุฉ
|
| 135 |
+
if preset.back_box_enabled and box_highlight_type == "sentence":
|
| 136 |
+
box_hide = "" # ูุง ูุฎูู ุงูุจููุณ ุนู ุงูููู
ุฉ ุงููุดุทุฉ ูู ุทุจูุฉ ุงูุฎูููุฉ ูุฃูู ููุฌู
ูุฉ ูููุง
|
| 137 |
+
base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
|
| 138 |
events += f"Dialogue: 0,{w_start},{w_end},BackBox,,0,0,0,,{base_display_text}\n"
|
| 139 |
else:
|
| 140 |
+
# ุงููู
ุท ุงูุงูุชุฑุงุถู ุฃู 'word': ุงูุตูุฏูู ูุฎุชูู ู
ู ุงูุฎูููุฉ ููุธูุฑ ูู ุทุจูุฉ ุงููุงููุงูุช ููุท
|
| 141 |
+
box_hide = "\\4a&HFF&"
|
| 142 |
+
base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&{box_hide}}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
|
| 143 |
events += f"Dialogue: 0,{w_start},{w_end},Main,,0,0,0,,{base_display_text}\n"
|
| 144 |
|
| 145 |
+
# 2. ุทุจูุฉ ุงูููู
ุฉ ุงููุดุทุฉ (Highlight)
|
| 146 |
prefix_hl = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
|
| 147 |
suffix_hl = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
|
| 148 |
target_word_hl = words_list_base[i]
|
| 149 |
|
| 150 |
t1 = min(60, int(duration * 150))
|
| 151 |
t2 = t1 + 80
|
|
|
|
| 152 |
|
| 153 |
# ุฅุถุงูุฉ ุชุฃุซูุฑ ุงูุชููุฌ (Blur)
|
| 154 |
glow_val = getattr(preset, 'glow_intensity', 0)
|
| 155 |
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
|
| 156 |
|
|
|
|
| 157 |
if angle != 0:
|
| 158 |
rotation = f"\\frz{angle}"
|
| 159 |
else:
|
| 160 |
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
|
| 161 |
|
| 162 |
+
# ุฅุนุฏุงุฏุงุช ุงูุญุฑูุฉ ูุงูููู
|
| 163 |
+
scale = int(preset.pop_up_scale * 100)
|
| 164 |
+
if preset.back_box_enabled:
|
| 165 |
+
if box_highlight_type == "word":
|
| 166 |
+
# ุงูุตูุฏูู ุญูู ุงูููู
ุฉ ููุท: ูุณุชุฎุฏู
BackBox ู
ุน ุฅุธูุงุฑ ุงูุตูุฏูู ูุงูููู
ุฉ ุจูุถูุญ (ุดูุงููุฉ 00)
|
| 167 |
+
# \1c=ูุตุ \4c=ุตูุฏููุ \1a=ุดูุงููุฉ ูุตุ \4a=ุดูุงููุฉ ุตูุฏูู
|
| 168 |
+
color_tags = f"\\1c{primary}\\4c{secondary}\\1a&H00&\\4a&H00&"
|
| 169 |
+
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
|
| 170 |
+
# ุฅุฎูุงุก ุงููุต ูุงูุตูุฏูู ูู ุงูุฃุฌุฒุงุก ุบูุฑ ุงููุดุทุฉ
|
| 171 |
+
display_text_hl = f"{{\\alpha&HFF&\\4a&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&\\4a&HFF&}}{suffix_hl}"
|
| 172 |
+
events += f"Dialogue: 1,{w_start},{w_end},BackBox,,0,0,0,,{display_text_hl}\n"
|
| 173 |
+
else:
|
| 174 |
+
# ุงูุตูุฏูู ุญูู ุงูุฌู
ูุฉ: ุงููุงููุงูุช ูู ุชุบููุฑ ููู ุงููุต ู
ุน ุชุฃุซูุฑ Pop-up
|
| 175 |
+
color_tags = f"\\1c{secondary}\\1a&H00&"
|
| 176 |
+
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
|
| 177 |
+
display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
|
| 178 |
+
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
|
| 179 |
+
else:
|
| 180 |
+
# ุจุฏูู ุตูุงุฏูู ููุงุฆูุงู: ูุงููุงูุช ูุตู ููุท
|
| 181 |
+
color_tags = f"\\1c{secondary}\\1a&H00&"
|
| 182 |
+
anim = f"{{{color_tags}\\fscx{scale}\\fscy{scale}{rotation}{blur_tag}}}"
|
| 183 |
+
display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
|
| 184 |
+
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
|
| 185 |
|
| 186 |
with open(output_path, "w", encoding="utf-8-sig") as f:
|
| 187 |
f.write(header + events)
|
routers/video.py
CHANGED
|
@@ -145,7 +145,7 @@ def background_processing(
|
|
| 145 |
if f and os.path.exists(f) and TEMP_DIR in f:
|
| 146 |
safe_remove(f)
|
| 147 |
|
| 148 |
-
@router.get("/
|
| 149 |
async def get_clear_preview(folder: str = "temp"):
|
| 150 |
"""
|
| 151 |
Preview files that will be deleted before actually clearing them.
|
|
@@ -433,7 +433,7 @@ async def list_saved_files():
|
|
| 433 |
|
| 434 |
@router.api_route("/clear", methods=["GET", "POST"], tags=["Video"])
|
| 435 |
async def clear_temp_files(
|
| 436 |
-
folder: str = Query("temp", description="Folder to clear", enum=["temp", "processed", "
|
| 437 |
specific_files: str = Query(None, description="Comma-separated list of specific files to delete (optional). Use /clear-preview to see available files.")
|
| 438 |
):
|
| 439 |
"""
|
|
@@ -452,8 +452,7 @@ async def clear_temp_files(
|
|
| 452 |
target_folders = [TEMP_DIR]
|
| 453 |
elif folder == "processed":
|
| 454 |
target_folders = [PROCESSED_DIR]
|
| 455 |
-
|
| 456 |
-
target_folders = [AUDIO_DIR]
|
| 457 |
elif folder == "bgmusic":
|
| 458 |
target_folders = [BG_MUSIC_DIR]
|
| 459 |
elif folder == "originals":
|
|
@@ -461,7 +460,7 @@ async def clear_temp_files(
|
|
| 461 |
else:
|
| 462 |
return JSONResponse(
|
| 463 |
status_code=400,
|
| 464 |
-
content={"error": f"Invalid folder: {folder}. Use: temp, processed,
|
| 465 |
)
|
| 466 |
|
| 467 |
deleted_files = []
|
|
@@ -640,6 +639,7 @@ async def process_saved_video(
|
|
| 640 |
original_filename: str = Form(..., description="ุงุณู
ุงูููุฏูู ุงูุฃุตูู ุงูู
ุญููุธ"),
|
| 641 |
aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
|
| 642 |
style: ShortsStyle = Form(ShortsStyle.CINEMATIC, description="Select visual style"),
|
|
|
|
| 643 |
background_music_filename: str = Form(None, description="Name of a saved background music file (optional)"),
|
| 644 |
video_volume: float = Form(1.0, description="Volume of original video"),
|
| 645 |
music_volume: float = Form(0.2, description="Volume of background music"),
|
|
@@ -647,6 +647,7 @@ async def process_saved_video(
|
|
| 647 |
timestamps_json: str = Form(None, alias="timestamps", description='JSON list of timestamps.'),
|
| 648 |
export_audio: bool = Form(False, description="Export separate audio files"),
|
| 649 |
webhook_url: str = Form(None, description="Optional URL for results"),
|
|
|
|
| 650 |
transcription_json: str = Form(None, alias="transcription", description="JSON string of transcription data"),
|
| 651 |
subtitle_style: str = Form(None, description="Name of the saved subtitle preset"),
|
| 652 |
subtitle_preset_json: str = Form(None, alias="subtitle_preset", description="Custom subtitle preset JSON override")
|
|
@@ -671,9 +672,13 @@ async def process_saved_video(
|
|
| 671 |
temp_path = os.path.join(TEMP_DIR, f"{task_id}_temp_{original_filename}")
|
| 672 |
shutil.copy2(original_path, temp_path)
|
| 673 |
|
| 674 |
-
# Handle background music
|
| 675 |
audio_path = None
|
| 676 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
audio_path = os.path.join(BG_MUSIC_DIR, background_music_filename)
|
| 678 |
if not os.path.exists(audio_path):
|
| 679 |
print(f"Warning: Background music {background_music_filename} not found.")
|
|
|
|
| 145 |
if f and os.path.exists(f) and TEMP_DIR in f:
|
| 146 |
safe_remove(f)
|
| 147 |
|
| 148 |
+
@router.get("/privevies-files", tags=["Video"])
|
| 149 |
async def get_clear_preview(folder: str = "temp"):
|
| 150 |
"""
|
| 151 |
Preview files that will be deleted before actually clearing them.
|
|
|
|
| 433 |
|
| 434 |
@router.api_route("/clear", methods=["GET", "POST"], tags=["Video"])
|
| 435 |
async def clear_temp_files(
|
| 436 |
+
folder: str = Query("temp", description="Folder to clear", enum=["temp", "processed", "bgmusic", "originals", "all"]),
|
| 437 |
specific_files: str = Query(None, description="Comma-separated list of specific files to delete (optional). Use /clear-preview to see available files.")
|
| 438 |
):
|
| 439 |
"""
|
|
|
|
| 452 |
target_folders = [TEMP_DIR]
|
| 453 |
elif folder == "processed":
|
| 454 |
target_folders = [PROCESSED_DIR]
|
| 455 |
+
|
|
|
|
| 456 |
elif folder == "bgmusic":
|
| 457 |
target_folders = [BG_MUSIC_DIR]
|
| 458 |
elif folder == "originals":
|
|
|
|
| 460 |
else:
|
| 461 |
return JSONResponse(
|
| 462 |
status_code=400,
|
| 463 |
+
content={"error": f"Invalid folder: {folder}. Use: temp, processed, bgmusic, originals, or all"}
|
| 464 |
)
|
| 465 |
|
| 466 |
deleted_files = []
|
|
|
|
| 639 |
original_filename: str = Form(..., description="ุงุณู
ุงูููุฏูู ุงูุฃุตูู ุงูู
ุญููุธ"),
|
| 640 |
aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
|
| 641 |
style: ShortsStyle = Form(ShortsStyle.CINEMATIC, description="Select visual style"),
|
| 642 |
+
background_music: UploadFile = File(None, description="Upload a new background music file"),
|
| 643 |
background_music_filename: str = Form(None, description="Name of a saved background music file (optional)"),
|
| 644 |
video_volume: float = Form(1.0, description="Volume of original video"),
|
| 645 |
music_volume: float = Form(0.2, description="Volume of background music"),
|
|
|
|
| 647 |
timestamps_json: str = Form(None, alias="timestamps", description='JSON list of timestamps.'),
|
| 648 |
export_audio: bool = Form(False, description="Export separate audio files"),
|
| 649 |
webhook_url: str = Form(None, description="Optional URL for results"),
|
| 650 |
+
return_files: bool = Form(False, description="Return processed files directly"),
|
| 651 |
transcription_json: str = Form(None, alias="transcription", description="JSON string of transcription data"),
|
| 652 |
subtitle_style: str = Form(None, description="Name of the saved subtitle preset"),
|
| 653 |
subtitle_preset_json: str = Form(None, alias="subtitle_preset", description="Custom subtitle preset JSON override")
|
|
|
|
| 672 |
temp_path = os.path.join(TEMP_DIR, f"{task_id}_temp_{original_filename}")
|
| 673 |
shutil.copy2(original_path, temp_path)
|
| 674 |
|
| 675 |
+
# Handle background music (New upload takes priority)
|
| 676 |
audio_path = None
|
| 677 |
+
if background_music:
|
| 678 |
+
audio_path = os.path.join(BG_MUSIC_DIR, f"bg_{task_id}_{background_music.filename}")
|
| 679 |
+
with open(audio_path, "wb") as buffer:
|
| 680 |
+
shutil.copyfileobj(background_music.file, buffer)
|
| 681 |
+
elif background_music_filename:
|
| 682 |
audio_path = os.path.join(BG_MUSIC_DIR, background_music_filename)
|
| 683 |
if not os.path.exists(audio_path):
|
| 684 |
print(f"Warning: Background music {background_music_filename} not found.")
|
tests/test_comprehensive_e2e.py
CHANGED
|
@@ -29,107 +29,139 @@ def create_mock_video_if_needed(filename="0eb30aa9_original_my_movie.mp4"):
|
|
| 29 |
], capture_output=True)
|
| 30 |
return filename
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def test_full_workflow():
|
| 33 |
-
print("\n
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
# 1.
|
| 36 |
-
preset_name = "e2e_sentence_style"
|
| 37 |
-
print(f"Step 1: Saving preset '{preset_name}' with display_mode='sentence'...")
|
| 38 |
-
preset_data = {
|
| 39 |
-
"name": (None, preset_name),
|
| 40 |
-
"font_name": (None, "Arial"),
|
| 41 |
-
"font_size": (None, "55"),
|
| 42 |
-
"primary_color": (None, "#FFFFFF"),
|
| 43 |
-
"secondary_color": (None, "#00FF00"), # Green Highlight
|
| 44 |
-
"display_mode": (None, "sentence"), # ุงูู
ูุฒุฉ ุงูุฌุฏูุฏุฉ
|
| 45 |
-
"max_words_per_line": (None, "4"), # ุงูู
ูุฒุฉ ุงูุฌุฏูุฏุฉ
|
| 46 |
-
"back_box_enabled": (None, "true")
|
| 47 |
-
}
|
| 48 |
-
resp = client.post("/api/presets/save", data=preset_data)
|
| 49 |
-
assert resp.status_code == 200, f"Failed to save preset: {resp.text}"
|
| 50 |
-
print("โ
Sentence preset saved successfully.")
|
| 51 |
-
|
| 52 |
-
# 2. Verify Preset List
|
| 53 |
-
print("Step 2: Verifying preset list...")
|
| 54 |
-
resp = client.get("/api/presets/list")
|
| 55 |
-
assert preset_name in resp.json(), "Preset not found in list"
|
| 56 |
-
print(f"โ
Preset '{preset_name}' is in the list.")
|
| 57 |
-
|
| 58 |
-
# 3. Create/Ensure original video exists
|
| 59 |
video_name = create_mock_video_if_needed()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
| 62 |
-
print(f"Step 3: Testing Manual Sentence Timing Sync with '{preset_name}'...")
|
| 63 |
-
# ุจูุงูุงุช ุจุชูุณูู SentenceTiming (ุฌู
ูุฉ ุจุฏุงุฎููุง ููู
ุงุช)
|
| 64 |
-
transcription_new = [
|
| 65 |
{
|
| 66 |
-
"text": "
|
| 67 |
"start": 0.0,
|
| 68 |
-
"end": 3.0,
|
| 69 |
-
"words": [
|
| 70 |
-
{"word": "This", "start": 0.0, "end": 0.5},
|
| 71 |
-
{"word": "is", "start": 0.5, "end": 1.0},
|
| 72 |
-
{"word": "the", "start": 1.0, "end": 1.5},
|
| 73 |
-
{"word": "new", "start": 1.5, "end": 2.2},
|
| 74 |
-
{"word": "sync", "start": 2.2, "end": 2.6},
|
| 75 |
-
{"word": "system", "start": 2.6, "end": 3.0}
|
| 76 |
-
]
|
| 77 |
-
},
|
| 78 |
-
{
|
| 79 |
-
"text": "It works perfectly with presets",
|
| 80 |
-
"start": 3.0,
|
| 81 |
"end": 5.0,
|
| 82 |
"words": [
|
| 83 |
-
{"word": "
|
| 84 |
-
{"word": "
|
| 85 |
-
{"word": "
|
| 86 |
-
{"word": "
|
| 87 |
-
{"word": "
|
| 88 |
]
|
| 89 |
}
|
| 90 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
"
|
| 97 |
-
"
|
| 98 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
#
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
{"word": "Old", "start": 0.0, "end": 1.0},
|
| 111 |
-
{"word": "format", "start": 1.0, "end": 2.0},
|
| 112 |
-
{"word": "still", "start": 2.0, "end": 3.0},
|
| 113 |
-
{"word": "works", "start": 3.0, "end": 5.0}
|
| 114 |
-
]
|
| 115 |
-
process_data["transcription"] = json.dumps(transcription_old)
|
| 116 |
-
process_data["subtitle_style"] = "gaming" # ุงุณุชุฎุฏุงู
ุจุฑูุณูุช ุงูุชุฑุงุถู ูุฏูู
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
assert resp.status_code == 200
|
| 120 |
-
print("โ
|
| 121 |
-
|
| 122 |
-
# 6. Check File Listing
|
| 123 |
-
print("Step 5: Checking file listing...")
|
| 124 |
-
resp = client.get("/api/video/list-saved")
|
| 125 |
-
files = resp.json()["files"]
|
| 126 |
-
assert len(files["processed"]) >= 2
|
| 127 |
-
print(f"โ
Found {len(files['processed'])} processed files.")
|
| 128 |
|
| 129 |
-
#
|
| 130 |
-
print("Step 6: Cleaning up test preset...")
|
| 131 |
client.delete(f"/api/presets/delete/{preset_name}")
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
if __name__ == "__main__":
|
| 135 |
test_full_workflow()
|
|
|
|
| 29 |
], capture_output=True)
|
| 30 |
return filename
|
| 31 |
|
| 32 |
+
def create_mock_audio():
|
| 33 |
+
audio_filename = "test_bg_music.mp3"
|
| 34 |
+
path = os.path.join(TEMP_DIR, audio_filename)
|
| 35 |
+
if not os.path.exists(path):
|
| 36 |
+
print(f"Creating mock audio at {path}...")
|
| 37 |
+
import imageio_ffmpeg
|
| 38 |
+
import subprocess
|
| 39 |
+
exe = imageio_ffmpeg.get_ffmpeg_exe()
|
| 40 |
+
subprocess.run([
|
| 41 |
+
exe, '-f', 'lavfi', '-i', 'sine=frequency=440:duration=5',
|
| 42 |
+
'-t', '5', path, '-y'
|
| 43 |
+
], capture_output=True)
|
| 44 |
+
return path
|
| 45 |
+
|
| 46 |
def test_full_workflow():
|
| 47 |
+
print("\n" + "="*50)
|
| 48 |
+
print("๐ STARTING MULTI-PRESET CINEMATIC E2E TEST")
|
| 49 |
+
print("="*50)
|
| 50 |
|
| 51 |
+
# 1. Ensure mock assets exist
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
video_name = create_mock_video_if_needed()
|
| 53 |
+
audio_file_path = create_mock_audio()
|
| 54 |
+
|
| 55 |
+
# List of default presets to test
|
| 56 |
+
default_presets = ["youtube_casual", "gaming", "professional", "storyteller"]
|
| 57 |
|
| 58 |
+
transcription = [
|
|
|
|
|
|
|
|
|
|
| 59 |
{
|
| 60 |
+
"text": "Testing all default presets with cinematic style",
|
| 61 |
"start": 0.0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
"end": 5.0,
|
| 63 |
"words": [
|
| 64 |
+
{"word": "Testing", "start": 0.0, "end": 1.0},
|
| 65 |
+
{"word": "all", "start": 1.0, "end": 1.5},
|
| 66 |
+
{"word": "default", "start": 1.5, "end": 2.5},
|
| 67 |
+
{"word": "presets", "start": 2.5, "end": 3.5},
|
| 68 |
+
{"word": "cinematic", "start": 3.5, "end": 5.0}
|
| 69 |
]
|
| 70 |
}
|
| 71 |
]
|
| 72 |
+
|
| 73 |
+
for preset in default_presets:
|
| 74 |
+
print(f"\n[Feature Test] ๐ฌ Cinematic Style + ๐จ Preset: {preset}")
|
| 75 |
+
|
| 76 |
+
process_data = {
|
| 77 |
+
"original_filename": (None, video_name),
|
| 78 |
+
"aspect_ratio": (None, "9:16"),
|
| 79 |
+
"style": (None, "cinematic"),
|
| 80 |
+
"subtitle_style": (None, preset),
|
| 81 |
+
"transcription": (None, json.dumps(transcription)),
|
| 82 |
+
"timestamps": (None, json.dumps([{"start_time": 0, "end_time": 5}])),
|
| 83 |
+
"music_volume": (None, "0.3"),
|
| 84 |
+
"video_volume": (None, "1.0"),
|
| 85 |
+
"loop_music": (None, "true")
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
with open(audio_file_path, "rb") as f:
|
| 89 |
+
files = {"background_music": ("bg.mp3", f, "audio/mpeg")}
|
| 90 |
+
resp = client.post("/api/video/process-saved", data=process_data, files=files)
|
| 91 |
+
|
| 92 |
+
assert resp.status_code == 200, f"Process failed for preset {preset}: {resp.text}"
|
| 93 |
+
print(f"โ
Preset '{preset}' processed successfully in Cinematic style.")
|
| 94 |
+
|
| 95 |
+
# 2. Test Custom Pro Preset with Background Box Color
|
| 96 |
+
# 2. Test Word-Based Box Highlight (Box moves with word)
|
| 97 |
+
print(f"\n[Feature Test] ๐ Word-Based Box Highlight (Moving Box)")
|
| 98 |
+
word_preset_name = "word_box_moving"
|
| 99 |
+
word_preset_data = {
|
| 100 |
+
"name": (None, word_preset_name),
|
| 101 |
+
"font_name": (None, "Impact"),
|
| 102 |
+
"font_size": "60",
|
| 103 |
+
"primary_color": (None, "#FFFFFF"), # ูุต ุฃุจูุถ
|
| 104 |
+
"secondary_color": (None, "#0000FF"), # ุตูุฏูู ุฃุฒุฑู
|
| 105 |
+
"box_highlight_type": (None, "word"), # ุงูุตูุฏูู ูุชุญุฑู ู
ุน ุงูููู
ุฉ
|
| 106 |
+
"back_box_enabled": (None, "true"),
|
| 107 |
+
"pop_up_scale": (None, "1.3"), # ุจูุจ ุฃุจ ููููู
ุฉ
|
| 108 |
+
"background_opacity": (None, "0.8")
|
| 109 |
+
}
|
| 110 |
+
client.post("/api/presets/save", data=word_preset_data)
|
| 111 |
|
| 112 |
+
# 3. Test Sentence-Based Box Highlight (Static Box + Word Popup)
|
| 113 |
+
print(f"\n[Feature Test] ๐ผ๏ธ Sentence-Based Box Highlight (Static Box + Word Popup)")
|
| 114 |
+
sent_preset_name = "sent_box_static"
|
| 115 |
+
sent_preset_data = {
|
| 116 |
+
"name": (None, sent_preset_name),
|
| 117 |
+
"font_name": (None, "Arial"),
|
| 118 |
+
"font_size": "55",
|
| 119 |
+
"primary_color": (None, "#FFFFFF"),
|
| 120 |
+
"secondary_color": (None, "#00FF00"), # ูุงููุงูุช ุฃุฎุถุฑ
|
| 121 |
+
"box_highlight_type": (None, "sentence"), # ุตูุฏูู ุญูู ุงูุฌู
ูุฉ ูููุง
|
| 122 |
+
"box_rounding": (None, "15"),
|
| 123 |
+
"back_box_enabled": (None, "true"),
|
| 124 |
+
"pop_up_scale": (None, "1.4"), # ุจูุจ ุฃุจ ููููู
ุฉ ุฏุงุฎู ุงูุตูุฏูู
|
| 125 |
+
"background_opacity": (None, "0.6")
|
| 126 |
}
|
| 127 |
+
client.post("/api/presets/save", data=sent_preset_data)
|
| 128 |
|
| 129 |
+
# Run a test for both
|
| 130 |
+
for p_name in [word_preset_name, sent_preset_name]:
|
| 131 |
+
process_data = {
|
| 132 |
+
"original_filename": (None, video_name),
|
| 133 |
+
"aspect_ratio": (None, "9:16"),
|
| 134 |
+
"style": (None, "cinematic"),
|
| 135 |
+
"subtitle_style": (None, p_name),
|
| 136 |
+
"transcription": (None, json.dumps(transcription))
|
| 137 |
+
}
|
| 138 |
+
resp = client.post("/api/video/process-saved", data=process_data)
|
| 139 |
+
assert resp.status_code == 200
|
| 140 |
+
print(f"โ
Tested {p_name} successfully.")
|
| 141 |
|
| 142 |
+
# Cleanup
|
| 143 |
+
client.delete(f"/api/presets/delete/{word_preset_name}")
|
| 144 |
+
client.delete(f"/api/presets/delete/{sent_preset_name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
process_data_custom = {
|
| 147 |
+
"original_filename": (None, video_name),
|
| 148 |
+
"aspect_ratio": (None, "9:16"),
|
| 149 |
+
"style": (None, "cinematic"),
|
| 150 |
+
"subtitle_style": (None, preset_name),
|
| 151 |
+
"transcription": (None, json.dumps(transcription))
|
| 152 |
+
}
|
| 153 |
+
resp = client.post("/api/video/process-saved", data=process_data_custom)
|
| 154 |
assert resp.status_code == 200
|
| 155 |
+
print(f"โ
Custom Preset with Blue Box processed successfully.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
# 3. Cleanup
|
|
|
|
| 158 |
client.delete(f"/api/presets/delete/{preset_name}")
|
| 159 |
+
if os.path.exists(audio_file_path):
|
| 160 |
+
os.remove(audio_file_path)
|
| 161 |
+
|
| 162 |
+
print("\n" + "="*50)
|
| 163 |
+
print("๐ ALL MULTI-PRESET TESTS PASSED!")
|
| 164 |
+
print("="*50)
|
| 165 |
|
| 166 |
if __name__ == "__main__":
|
| 167 |
test_full_workflow()
|