archivartaunik commited on
Commit
eb0efcf
·
verified ·
1 Parent(s): bc0ca7d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -99
app.py CHANGED
@@ -146,8 +146,7 @@ def parse_srt_from_text(srt_text):
146
  start_str, end_str = times.split("-->")
147
  start = srt_time_to_sec(start_str)
148
  end = srt_time_to_sec(end_str)
149
- # захоўваем пераносы
150
- text = "\n".join(lines[2:]).strip()
151
  subs.append({"start": start, "end": end, "text": text})
152
  except Exception:
153
  continue
@@ -158,13 +157,11 @@ def parse_srt_from_text(srt_text):
158
  # Wrap: аўтаматычны перанос радкоў па шырыні кадра
159
  # ----------------------------
160
  def _line_width_px(draw: ImageDraw.ImageDraw, line: str, font: ImageFont.ImageFont, stroke_width: int = 0) -> int:
161
- """Вяртае шырыню радка ў пікселях (з улікам stroke), максімальна сумяшчальна з Pillow."""
162
  try:
163
  l, t, r, b = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
164
  return int(r - l)
165
  except Exception:
166
  try:
167
- # textlength не ўлічвае stroke, таму дадамо запас
168
  w = draw.textlength(line, font=font)
169
  return int(w + 2 * stroke_width)
170
  except Exception:
@@ -173,7 +170,6 @@ def _line_width_px(draw: ImageDraw.ImageDraw, line: str, font: ImageFont.ImageFo
173
 
174
 
175
  def _break_long_word(draw, word: str, font, max_width: int, stroke_width: int) -> list:
176
- """Калі слова вельмі доўгае — разбіваем па сімвалах (greedy)."""
177
  chunks = []
178
  cur = ""
179
  for ch in word:
@@ -190,10 +186,6 @@ def _break_long_word(draw, word: str, font, max_width: int, stroke_width: int) -
190
 
191
  def wrap_text_to_width(text: str, draw: ImageDraw.ImageDraw, font: ImageFont.ImageFont,
192
  max_width: int, stroke_width: int = 0) -> str:
193
- """
194
- Аўта-перанос: захоўвае існуючыя '\n' як абзацы,
195
- але ўнутры кожнага радка робіць word-wrap пад max_width.
196
- """
197
  if not text:
198
  return text
199
 
@@ -210,7 +202,6 @@ def wrap_text_to_width(text: str, draw: ImageDraw.ImageDraw, font: ImageFont.Ima
210
  line = ""
211
  for w in words:
212
  if not line:
213
- # калі слова занадта доўгае — разбіваем
214
  if _line_width_px(draw, w, font, stroke_width) <= max_width:
215
  line = w
216
  else:
@@ -224,7 +215,6 @@ def wrap_text_to_width(text: str, draw: ImageDraw.ImageDraw, font: ImageFont.Ima
224
  line = test
225
  else:
226
  out_lines.append(line)
227
- # пачынаем новы радок
228
  if _line_width_px(draw, w, font, stroke_width) <= max_width:
229
  line = w
230
  else:
@@ -264,6 +254,10 @@ def _measure_multiline_bbox(draw, text, font, stroke_width=0, spacing=4):
264
  return (0, 0, w, h)
265
 
266
 
 
 
 
 
267
  def create_animated_text_clip(
268
  text,
269
  duration,
@@ -273,14 +267,14 @@ def create_animated_text_clip(
273
  stroke_color,
274
  stroke_width,
275
  position_type,
276
- custom_x,
277
  custom_y,
278
  animation,
279
  video_width,
280
  video_height,
281
  bg_color=None,
282
  bg_opacity=1.0,
283
- wrap_ratio: float = 0.90, # 90% ад шырыні кадра
284
  ):
285
  try:
286
  txt_rgb = hex_to_rgb(color)
@@ -313,7 +307,6 @@ def create_animated_text_clip(
313
  spacing = max(0, int(fontsize * 0.15))
314
 
315
  # --- AUTO WRAP ---
316
- # Макс. шырыня тэксту = wrap_ratio * video_width (але мінімум 200px)
317
  max_text_width = max(200, int(video_width * float(wrap_ratio)))
318
  wrapped_text = wrap_text_to_width(text, dummy_draw, pil_font, max_text_width, stroke_width=stroke_width)
319
 
@@ -332,7 +325,7 @@ def create_animated_text_clip(
332
  img = Image.new("RGBA", (img_w, img_h), bg_rgb + (bg_a,))
333
  draw = ImageDraw.Draw(img)
334
 
335
- # галоўнае: улічваем bbox (можа быць адмоўны) -> не рэжа
336
  text_x = (pad_x + safe) - l
337
  text_y = (pad_y + safe) - t
338
 
@@ -355,19 +348,28 @@ def create_animated_text_clip(
355
  mask = mpe.ImageClip(alpha, ismask=True).set_duration(duration)
356
  clip = clip.set_mask(mask)
357
 
 
 
 
 
 
358
  if position_type == "bottom":
359
- x = (video_width - clip.w) / 2
360
- y = video_height - clip.h - 20
361
  elif position_type == "top":
362
- x = (video_width - clip.w) / 2
363
- y = 20
364
  elif position_type == "center":
365
- x = (video_width - clip.w) / 2
366
  y = (video_height - clip.h) / 2
367
- else:
368
- x = float(custom_x)
369
  y = float(custom_y)
370
 
 
 
 
 
371
  anim = (animation or "").lower()
372
 
373
  if anim == "fade":
@@ -401,7 +403,7 @@ def apply_subtitles(
401
  stroke_color,
402
  stroke_width,
403
  position_type,
404
- custom_x,
405
  custom_y,
406
  animation,
407
  export_quality,
@@ -427,7 +429,7 @@ def apply_subtitles(
427
  stroke_color,
428
  stroke_width,
429
  position_type,
430
- custom_x,
431
  custom_y,
432
  animation,
433
  w,
@@ -476,7 +478,7 @@ def create_single_frame_video(
476
  stroke_color,
477
  stroke_width,
478
  position_type,
479
- custom_x,
480
  custom_y,
481
  animation,
482
  bg_color=None,
@@ -498,7 +500,7 @@ def create_single_frame_video(
498
  stroke_color,
499
  stroke_width,
500
  position_type,
501
- custom_x,
502
  custom_y,
503
  animation,
504
  w,
@@ -540,28 +542,81 @@ CSS = f"""
540
 
541
 
542
  # ----------------------------
543
- # Gradio UI
544
  # ----------------------------
545
  with gr.Blocks(css=CSS) as demo:
546
  gr.Markdown("# Аўтаматычнае стварэнне і накладанне субтытраў")
547
 
548
- # адзін плэер: загрузка/передпрагляд/вынік
549
- video_input = gr.Video(
550
- label="Загрузіце відэа (тут жа будзе перадпрагляд і вынік)",
551
- elem_id="main_video",
552
- )
553
-
554
  uploaded_video = gr.State(None)
555
  first_frame = gr.State(None)
556
 
557
- with gr.Row():
558
- gen_btn = gr.Button("Стварыць субтытры")
559
- subs_box = gr.Textbox(label="Тэкст субтытраў", lines=15)
560
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  def _on_video_change(v, current_uploaded, current_frame):
562
  if not v:
563
  return None, None
564
 
 
 
 
 
565
  name = os.path.basename(str(v))
566
  if name in ("preview_video.mp4", "output_video.mp4"):
567
  return current_uploaded, current_frame
@@ -575,6 +630,13 @@ with gr.Blocks(css=CSS) as demo:
575
  outputs=[uploaded_video, first_frame],
576
  )
577
 
 
 
 
 
 
 
 
578
  def _on_generate_click(v_orig):
579
  if not v_orig:
580
  return "Не выбрана відэа"
@@ -583,41 +645,15 @@ with gr.Blocks(css=CSS) as demo:
583
 
584
  gen_btn.click(_on_generate_click, inputs=uploaded_video, outputs=subs_box)
585
 
586
- gr.Markdown("## Наладка стылю субтытраў")
587
-
588
- with gr.Row():
589
- font_dd = gr.Dropdown(list(AVAILABLE_FONTS.keys()), value="DejaVuSans-Bold", label="Шрыфт")
590
- size_sl = gr.Slider(10, 100, 1, value=40, label="Памер шрыфта")
591
-
592
- with gr.Row():
593
- color_txt = gr.ColorPicker(value="#FFFFFF", label="Колер тэксту")
594
- color_st = gr.ColorPicker(value="#000000", label="Колер абводкі")
595
- width_sl = gr.Slider(0, 10, 1, value=2, label="Таўшчыня абводкі")
596
-
597
- with gr.Row():
598
- pos_dd = gr.Dropdown(["bottom", "top", "center", "custom"], value="bottom", label="Пазіцыя")
599
- anim_dd = gr.Dropdown(["None", "fade", "slide", "zoom"], value="None", label="Анімацыя")
600
-
601
- with gr.Row():
602
- x_sl = gr.Slider(0, 1920, 1, value=100, label="X (для custom)")
603
- y_sl = gr.Slider(0, 1080, 1, value=100, label="Y (для custom)")
604
 
605
- with gr.Row():
606
- bg_color = gr.ColorPicker(value="#000000", label="Колер фону")
607
- bg_opac = gr.Slider(0.0, 1.0, 0.1, value=0.5, label="Празрыстасць фону")
608
 
609
- # ДАДАЛІ: кіраванне wrap
610
- with gr.Row():
611
- wrap_en = gr.Checkbox(value=True, label="Аўта-перанос радкоў")
612
- wrap_pct = gr.Slider(50, 100, 1, value=90, label="Макс. шырыня радка (%)")
613
-
614
- exp_dd = gr.Dropdown(
615
- ["мінімальнае", "арыгінальнае", "сярэдняе", "максімальнае"],
616
- value="арыгінальнае",
617
- label="Якасць",
618
- )
619
-
620
- def update_preview(frame, text, font, size, col, colst, wd, pos, x, y, anim, bgc, bgo, do_wrap, w_pct):
621
  if frame is None:
622
  return None
623
 
@@ -636,7 +672,7 @@ with gr.Blocks(css=CSS) as demo:
636
  colst,
637
  wd,
638
  pos,
639
- x,
640
  y,
641
  anim,
642
  bg_color=bgc,
@@ -644,30 +680,29 @@ with gr.Blocks(css=CSS) as demo:
644
  wrap_ratio=wrap_ratio,
645
  )
646
 
647
- comps = [font_dd, size_sl, color_txt, color_st, width_sl, pos_dd, anim_dd, subs_box, x_sl, y_sl, bg_color, bg_opac, wrap_en, wrap_pct]
648
- for c in comps:
649
- c.change(
650
- update_preview,
651
- inputs=[
652
- first_frame,
653
- subs_box,
654
- font_dd,
655
- size_sl,
656
- color_txt,
657
- color_st,
658
- width_sl,
659
- pos_dd,
660
- x_sl,
661
- y_sl,
662
- anim_dd,
663
- bg_color,
664
- bg_opac,
665
- wrap_en,
666
- wrap_pct,
667
- ],
668
- outputs=video_input,
669
- )
670
-
671
  def _on_apply(
672
  v_orig,
673
  t,
@@ -677,7 +712,7 @@ with gr.Blocks(css=CSS) as demo:
677
  cs,
678
  wd,
679
  p,
680
- x,
681
  y,
682
  a,
683
  eq,
@@ -699,7 +734,7 @@ with gr.Blocks(css=CSS) as demo:
699
  cs,
700
  wd,
701
  p,
702
- x,
703
  y,
704
  a,
705
  eq,
@@ -708,7 +743,6 @@ with gr.Blocks(css=CSS) as demo:
708
  wrap_ratio=wrap_ratio,
709
  )
710
 
711
- apply_btn = gr.Button("Накласці субтытры (фінальны вынік)")
712
  apply_btn.click(
713
  _on_apply,
714
  inputs=[
@@ -720,7 +754,7 @@ with gr.Blocks(css=CSS) as demo:
720
  color_st,
721
  width_sl,
722
  pos_dd,
723
- x_sl,
724
  y_sl,
725
  anim_dd,
726
  exp_dd,
 
146
  start_str, end_str = times.split("-->")
147
  start = srt_time_to_sec(start_str)
148
  end = srt_time_to_sec(end_str)
149
+ text = "\n".join(lines[2:]).strip() # захоўваем пераносы
 
150
  subs.append({"start": start, "end": end, "text": text})
151
  except Exception:
152
  continue
 
157
  # Wrap: аўтаматычны перанос радкоў па шырыні кадра
158
  # ----------------------------
159
  def _line_width_px(draw: ImageDraw.ImageDraw, line: str, font: ImageFont.ImageFont, stroke_width: int = 0) -> int:
 
160
  try:
161
  l, t, r, b = draw.textbbox((0, 0), line, font=font, stroke_width=stroke_width)
162
  return int(r - l)
163
  except Exception:
164
  try:
 
165
  w = draw.textlength(line, font=font)
166
  return int(w + 2 * stroke_width)
167
  except Exception:
 
170
 
171
 
172
  def _break_long_word(draw, word: str, font, max_width: int, stroke_width: int) -> list:
 
173
  chunks = []
174
  cur = ""
175
  for ch in word:
 
186
 
187
  def wrap_text_to_width(text: str, draw: ImageDraw.ImageDraw, font: ImageFont.ImageFont,
188
  max_width: int, stroke_width: int = 0) -> str:
 
 
 
 
189
  if not text:
190
  return text
191
 
 
202
  line = ""
203
  for w in words:
204
  if not line:
 
205
  if _line_width_px(draw, w, font, stroke_width) <= max_width:
206
  line = w
207
  else:
 
215
  line = test
216
  else:
217
  out_lines.append(line)
 
218
  if _line_width_px(draw, w, font, stroke_width) <= max_width:
219
  line = w
220
  else:
 
254
  return (0, 0, w, h)
255
 
256
 
257
+ def _clamp(v: float, lo: float, hi: float) -> float:
258
+ return max(lo, min(hi, v))
259
+
260
+
261
  def create_animated_text_clip(
262
  text,
263
  duration,
 
267
  stroke_color,
268
  stroke_width,
269
  position_type,
270
+ custom_x_shift, # ЗРУХ X (адносна цэнтра)
271
  custom_y,
272
  animation,
273
  video_width,
274
  video_height,
275
  bg_color=None,
276
  bg_opacity=1.0,
277
+ wrap_ratio: float = 0.90,
278
  ):
279
  try:
280
  txt_rgb = hex_to_rgb(color)
 
307
  spacing = max(0, int(fontsize * 0.15))
308
 
309
  # --- AUTO WRAP ---
 
310
  max_text_width = max(200, int(video_width * float(wrap_ratio)))
311
  wrapped_text = wrap_text_to_width(text, dummy_draw, pil_font, max_text_width, stroke_width=stroke_width)
312
 
 
325
  img = Image.new("RGBA", (img_w, img_h), bg_rgb + (bg_a,))
326
  draw = ImageDraw.Draw(img)
327
 
328
+ # галоўнае: улічваем bbox (можа быць адмоўны)
329
  text_x = (pad_x + safe) - l
330
  text_y = (pad_y + safe) - t
331
 
 
348
  mask = mpe.ImageClip(alpha, ismask=True).set_duration(duration)
349
  clip = clip.set_mask(mask)
350
 
351
+ # --- Пазіцыя (аптымізавана) ---
352
+ # верх/ніз ссоўваем да цэнтра на 15% вышыні кадра
353
+ center_pull = 0.15 * float(video_height)
354
+ base_x_center = (video_width - clip.w) / 2
355
+
356
  if position_type == "bottom":
357
+ x = base_x_center
358
+ y = (video_height - clip.h - 20) - center_pull
359
  elif position_type == "top":
360
+ x = base_x_center
361
+ y = 20 + center_pull
362
  elif position_type == "center":
363
+ x = base_x_center
364
  y = (video_height - clip.h) / 2
365
+ else: # custom: Х заўсёды ад цэнтра (як ва ўсіх), custom_x_shift — дадатковы зрух
366
+ x = base_x_center + float(custom_x_shift)
367
  y = float(custom_y)
368
 
369
+ # clamp, каб не вылятала за кадр
370
+ x = _clamp(x, 0, max(0, video_width - clip.w))
371
+ y = _clamp(y, 0, max(0, video_height - clip.h))
372
+
373
  anim = (animation or "").lower()
374
 
375
  if anim == "fade":
 
403
  stroke_color,
404
  stroke_width,
405
  position_type,
406
+ custom_x_shift,
407
  custom_y,
408
  animation,
409
  export_quality,
 
429
  stroke_color,
430
  stroke_width,
431
  position_type,
432
+ custom_x_shift,
433
  custom_y,
434
  animation,
435
  w,
 
478
  stroke_color,
479
  stroke_width,
480
  position_type,
481
+ custom_x_shift,
482
  custom_y,
483
  animation,
484
  bg_color=None,
 
500
  stroke_color,
501
  stroke_width,
502
  position_type,
503
+ custom_x_shift,
504
  custom_y,
505
  animation,
506
  w,
 
542
 
543
 
544
  # ----------------------------
545
+ # Gradio UI (аптымізаваны layout)
546
  # ----------------------------
547
  with gr.Blocks(css=CSS) as demo:
548
  gr.Markdown("# Аўтаматычнае стварэнне і накладанне субтытраў")
549
 
550
+ # Стан: арыгінальны файл (каб не губляць пры preview/output)
 
 
 
 
 
551
  uploaded_video = gr.State(None)
552
  first_frame = gr.State(None)
553
 
554
+ with gr.Row(equal_height=True):
555
+ with gr.Column(scale=6):
556
+ video_input = gr.Video(
557
+ label="Відэа (тут жа будзе перадпрагляд і вынік)",
558
+ elem_id="main_video",
559
+ )
560
+ with gr.Row():
561
+ show_original_btn = gr.Button("Паказаць арыгінал")
562
+ # (кнопка дадатковая, але карысная пасля preview)
563
+
564
+ with gr.Column(scale=5):
565
+ # 1) Стварэнне субтытраў — разварочваецца/згортваецца
566
+ with gr.Accordion("Стварэнне субтытраў", open=False):
567
+ gen_btn = gr.Button("Стварыць субтытры")
568
+ subs_box = gr.Textbox(label="Тэкст субтытраў", lines=14)
569
+
570
+ # 2) Стыль
571
+ with gr.Accordion("Стыль субтытраў", open=True):
572
+ with gr.Row():
573
+ font_dd = gr.Dropdown(list(AVAILABLE_FONTS.keys()), value="DejaVuSans-Bold", label="Шрыфт")
574
+ size_sl = gr.Slider(10, 100, 1, value=40, label="Памер")
575
+
576
+ with gr.Row():
577
+ color_txt = gr.ColorPicker(value="#FFFFFF", label="Колер тэксту")
578
+ color_st = gr.ColorPicker(value="#000000", label="Колер абводкі")
579
+ width_sl = gr.Slider(0, 10, 1, value=2, label="Абводка")
580
+
581
+ with gr.Row():
582
+ bg_color = gr.ColorPicker(value="#000000", label="Фон")
583
+ bg_opac = gr.Slider(0.0, 1.0, 0.1, value=0.5, label="Празрыстасць")
584
+
585
+ with gr.Row():
586
+ pos_dd = gr.Dropdown(["bottom", "top", "center", "custom"], value="bottom", label="Пазіцыя")
587
+ anim_dd = gr.Dropdown(["None", "fade", "slide", "zoom"], value="None", label="Анімацыя")
588
+
589
+ # Custom controls (паказваем толькі калі патрэбна)
590
+ with gr.Row():
591
+ x_shift_sl = gr.Slider(-960, 960, 1, value=0, label="X-зрух ад цэнтра", visible=False)
592
+ y_sl = gr.Slider(0, 1080, 1, value=100, label="Y (для custom)", visible=False)
593
+
594
+ with gr.Row():
595
+ wrap_en = gr.Checkbox(value=True, label="Аўта-перанос радкоў")
596
+ wrap_pct = gr.Slider(50, 100, 1, value=90, label="Макс. шырыня радка (%)")
597
+
598
+ # 3) Экспарт
599
+ with gr.Accordion("Экспарт", open=False):
600
+ exp_dd = gr.Dropdown(
601
+ ["мінімальнае", "арыгінальнае", "сярэдняе", "максімальнае"],
602
+ value="арыгінальнае",
603
+ label="Якасць",
604
+ )
605
+ apply_btn = gr.Button("Накласці субтытры (фінальны вынік)")
606
+
607
+ # --- Лагічныя апрацоўшчыкі ---
608
+
609
+ # Калі значэнне video_input змянілася:
610
+ # - калі гэта preview/output -> НЕ пераціраем uploaded_video
611
+ # - калі гэта арыгінал (загрузка) -> абнаўляем uploaded_video і first_frame
612
  def _on_video_change(v, current_uploaded, current_frame):
613
  if not v:
614
  return None, None
615
 
616
+ # калі проста вярнуліся да арыгінала праз кнопку і ён ужо вядомы — не пералічваем кадр
617
+ if current_uploaded and str(v) == str(current_uploaded):
618
+ return current_uploaded, current_frame
619
+
620
  name = os.path.basename(str(v))
621
  if name in ("preview_video.mp4", "output_video.mp4"):
622
  return current_uploaded, current_frame
 
630
  outputs=[uploaded_video, first_frame],
631
  )
632
 
633
+ # Паказаць арыгінал у тым жа плэеры
634
+ def _show_original(v_orig):
635
+ return v_orig
636
+
637
+ show_original_btn.click(_show_original, inputs=uploaded_video, outputs=video_input)
638
+
639
+ # Генерацыя субтытраў заўсёды па арыгінальным uploaded_video
640
  def _on_generate_click(v_orig):
641
  if not v_orig:
642
  return "Не выбрана відэа"
 
645
 
646
  gen_btn.click(_on_generate_click, inputs=uploaded_video, outputs=subs_box)
647
 
648
+ # Пераключэнне бачнасці custom-слайдэраў
649
+ def _toggle_custom(pos):
650
+ vis = (pos == "custom")
651
+ return gr.update(visible=vis), gr.update(visible=vis)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
+ pos_dd.change(_toggle_custom, inputs=pos_dd, outputs=[x_shift_sl, y_sl])
 
 
654
 
655
+ # Перадпрагляд: 1 секунда на першым кадры, у тым жа video_input
656
+ def update_preview(frame, text, font, size, col, colst, wd, pos, x_shift, y, anim, bgc, bgo, do_wrap, w_pct):
 
 
 
 
 
 
 
 
 
 
657
  if frame is None:
658
  return None
659
 
 
672
  colst,
673
  wd,
674
  pos,
675
+ x_shift,
676
  y,
677
  anim,
678
  bg_color=bgc,
 
680
  wrap_ratio=wrap_ratio,
681
  )
682
 
683
+ preview_inputs = [
684
+ first_frame,
685
+ subs_box,
686
+ font_dd,
687
+ size_sl,
688
+ color_txt,
689
+ color_st,
690
+ width_sl,
691
+ pos_dd,
692
+ x_shift_sl,
693
+ y_sl,
694
+ anim_dd,
695
+ bg_color,
696
+ bg_opac,
697
+ wrap_en,
698
+ wrap_pct,
699
+ ]
700
+
701
+ # Любая змена ў стылі/тэксце абнаўляе перадпрагляд
702
+ for c in [subs_box, font_dd, size_sl, color_txt, color_st, width_sl, pos_dd, x_shift_sl, y_sl, anim_dd, bg_color, bg_opac, wrap_en, wrap_pct]:
703
+ c.change(update_preview, inputs=preview_inputs, outputs=video_input)
704
+
705
+ # Фінальны рэндэр у тым жа плэеры, але па арыгінальным uploaded_video
 
706
  def _on_apply(
707
  v_orig,
708
  t,
 
712
  cs,
713
  wd,
714
  p,
715
+ x_shift,
716
  y,
717
  a,
718
  eq,
 
734
  cs,
735
  wd,
736
  p,
737
+ x_shift,
738
  y,
739
  a,
740
  eq,
 
743
  wrap_ratio=wrap_ratio,
744
  )
745
 
 
746
  apply_btn.click(
747
  _on_apply,
748
  inputs=[
 
754
  color_st,
755
  width_sl,
756
  pos_dd,
757
+ x_shift_sl,
758
  y_sl,
759
  anim_dd,
760
  exp_dd,