aliSaac510 commited on
Commit
7af5aaa
ยท
1 Parent(s): 8c64cc3
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": 1.0,
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": False,
49
  "display_mode": "sentence",
50
  "max_words_per_line": 2,
51
  "uppercase": True,
52
  "letter_spacing": 1.0,
53
- "background_opacity": 0.8,
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
- back_color = f"&H{back_alpha:02X}000000" # ู„ูˆู† ุฎู„ููŠุฉ ุฃุณูˆุฏ ู…ุน ุดูุงููŠุฉ ู…ุชุบูŠุฑุฉ
 
 
 
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
- base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
130
 
131
- if preset.back_box_enabled:
 
 
 
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. ุทุจู‚ุฉ ุงู„ูƒู„ู…ุงุช ุงู„ู†ุดุทุฉ (Highlight / Popup)
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
- anim = f"{{\\1c{secondary}\\1a&H00&\\fscx80\\fscy80{rotation}{blur_tag}\\t(0,{t1},0.3,\\fscx{scale}\\fscy{scale})\\t({t1},{t2},1.5,\\fscx100\\fscy100)}}"
156
-
157
- # ุงุณุชุฎุฏุงู… \alpha&HFF& ู„ุฅุฎูุงุก ุงู„ูƒู„ู…ุงุช ุบูŠุฑ ุงู„ู†ุดุทุฉ ููŠ ู‡ุฐู‡ ุงู„ุทุจู‚ุฉ ูู‚ุท
158
- display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
159
-
160
- events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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("/clear-preview", tags=["Video"])
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", "audio", "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,8 +452,7 @@ async def clear_temp_files(
452
  target_folders = [TEMP_DIR]
453
  elif folder == "processed":
454
  target_folders = [PROCESSED_DIR]
455
- elif folder == "audio":
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, audio, originals, or all"}
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 if name provided
675
  audio_path = None
676
- if background_music_filename:
 
 
 
 
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--- Starting Comprehensive E2E Test (New Sync System) ---")
 
 
34
 
35
- # 1. Create a Custom Preset with Sentence Mode
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
- # 4. Test Manual Sentence Timing Sync (The New System)
62
- print(f"Step 3: Testing Manual Sentence Timing Sync with '{preset_name}'...")
63
- # ุจูŠุงู†ุงุช ุจุชู†ุณูŠู‚ SentenceTiming (ุฌู…ู„ุฉ ุจุฏุงุฎู„ู‡ุง ูƒู„ู…ุงุช)
64
- transcription_new = [
65
  {
66
- "text": "This is the new sync system",
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": "It", "start": 3.0, "end": 3.5},
84
- {"word": "works", "start": 3.5, "end": 4.0},
85
- {"word": "perfectly", "start": 4.0, "end": 4.5},
86
- {"word": "with", "start": 4.5, "end": 4.8},
87
- {"word": "presets", "start": 4.8, "end": 5.0}
88
  ]
89
  }
90
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- process_data = {
93
- "original_filename": video_name,
94
- "aspect_ratio": "9:16",
95
- "style": "cinematic",
96
- "subtitle_style": preset_name,
97
- "transcription": json.dumps(transcription_new),
98
- "timestamps": json.dumps([{"start_time": 0, "end_time": 5}])
 
 
 
 
 
 
 
99
  }
 
100
 
101
- resp = client.post("/api/video/process-saved", data=process_data)
102
- assert resp.status_code == 200, f"Process-saved failed: {resp.text}"
103
- result = resp.json()
104
- assert result["status"] == "success"
105
- print(f"โœ… New Sync System processed successfully. Task ID: {result['task_id']}")
 
 
 
 
 
 
 
106
 
107
- # 5. Test Backward Compatibility (Old Word List Format)
108
- print("Step 4: Testing Backward Compatibility (Old Word List Format)...")
109
- transcription_old = [
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
- resp = client.post("/api/video/process-saved", data=process_data)
 
 
 
 
 
 
 
119
  assert resp.status_code == 200
120
- print("โœ… Backward compatibility verified successfully.")
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
- # 7. Cleanup
130
- print("Step 6: Cleaning up test preset...")
131
  client.delete(f"/api/presets/delete/{preset_name}")
132
- print("โœ… E2E Test completed successfully.")
 
 
 
 
 
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()