Tim13ekd commited on
Commit
0ffcbef
·
verified ·
1 Parent(s): 4717f77

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -69
app.py CHANGED
@@ -28,21 +28,17 @@ FONT_OPTIONS = list(FONT_MAP.keys())
28
  def get_font_path(font_name):
29
  """
30
  Gibt den tatsächlichen, existierenden Pfad für die ausgewählte Schriftart zurück.
31
- Falls der ausgewählte Pfad nicht existiert, wird ein Fallback verwendet.
32
  """
33
  requested_path = FONT_MAP.get(font_name)
34
 
35
- # 1. Wenn der angefragte Pfad existiert oder None (System Default) ist, verwende ihn
36
  if requested_path is None or os.path.exists(requested_path):
37
  return requested_path
38
 
39
- # 2. Fallback: Suche nach einem funktionierenden Pfad
40
  for name, path in FONT_MAP.items():
41
  if path and os.path.exists(path):
42
  print(f"Warnung: Ausgewählte Schriftart '{font_name}' nicht gefunden. Verwende Fallback: '{name}'")
43
  return path
44
 
45
- # 3. Letzter Fallback: None (System Default)
46
  print("Warnung: Keine bevorzugten Schriftarten gefunden. Verwende FFmpeg System Standard.")
47
  return None
48
 
@@ -70,41 +66,40 @@ def save_temp_audio(audio_file_path):
70
 
71
  def create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, style):
72
  """
73
- Erstellt den FFmpeg drawtext Filter für die Basisschicht (den gesamten Satzabschnitt),
74
- der für die volle Clip-Dauer sichtbar ist (stabile Positionierung).
75
  """
76
- # Standard-Stil
77
  base_params = {
78
  "fontcolor": "white",
79
- "borderw": 0, # Kein Rand
80
  "bordercolor": "black",
81
- "box": 0, "boxcolor": "",
82
  "fontsize": font_size
83
  }
84
 
85
  style_lower = style.lower()
86
 
87
- # SPEZIALFALL: Modern Style (graue, semi-transparente Hintergrundbox)
88
- if style_lower == "modern":
89
- base_params["box"] = 1
90
- # Dunkelgrau (0x444444) mit 60% Transparenz (@0.6)
91
- base_params["boxcolor"] = "[email protected]"
92
- base_params["borderw"] = 0 # Kein Text-Rand bei Hintergrundbox
93
- base_params["fontsize"] = font_size
94
-
95
- # SPEZIALFALL: Pop Style (schwarze Box)
96
- elif style_lower == "pop":
97
  base_params["box"] = 1
98
- base_params["boxcolor"] = "0x000000@0.6"
99
- base_params["fontsize"] = font_size * 1.1
100
 
101
- # Für andere Stile wird der Basistext nur als Schatten (borderw=2) gezeichnet
102
- elif style_lower in ["bold", "badge", "word"]:
103
- base_params["borderw"] = 2
 
 
 
 
104
 
105
  escaped_text = full_text.replace(':', FFMPEG_ESCAPE_CHAR + ':')
106
 
107
- # Filter für den gesamten Satz, sichtbar für die gesamte Clip-Dauer
108
  drawtext_filter = (
109
  f"drawtext=text='{escaped_text}':"
110
  f"fontcolor={base_params['fontcolor']}:"
@@ -116,19 +111,30 @@ def create_sentence_base_filter(full_text, duration_clip, font_option, font_size
116
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
117
  )
118
 
119
- # Fügt fontfile nur hinzu, wenn vorhanden
120
  if font_option:
121
- # font_option enthält bereits 'fontfile='
122
  drawtext_filter += f":{font_option}"
123
 
124
- drawtext_filter += f":enable='between(t, 0, {duration_clip})'"
 
 
 
 
 
 
 
 
125
  return drawtext_filter
126
 
127
 
128
  def create_highlight_word_filter(word, full_text, start_time, duration, font_option, font_size, y_pos, style):
129
  """
130
- Erstellt den FFmpeg drawtext Filter für die Highlight-Schicht (nur das aktive Wort).
 
131
  """
 
 
 
 
132
  word_end_time = start_time + duration
133
 
134
  # Alpha-Ausdruck für smooth Fade-In und Fade-Out der HIGHLIGHT-FARBE
@@ -144,42 +150,36 @@ def create_highlight_word_filter(word, full_text, start_time, duration, font_opt
144
  "fontcolor": "yellow",
145
  "borderw": 0,
146
  "bordercolor": "black",
147
- "box": 0, "boxcolor": "",
148
  "fontsize_override": font_size * 1.05 # Leicht vergrößert
149
  }
150
 
151
  style_lower = style.lower()
152
 
153
- if style_lower == "modern":
154
- # Modern: Gelbe Schrift, kein Rand
155
- params["fontcolor"] = "yellow"
156
- params["borderw"] = 0
157
-
158
- elif style_lower == "bold":
159
- # Bold: Gelb mit starkem Rand
160
  params["fontcolor"] = "yellow"
161
  params["borderw"] = 4
162
-
163
- elif style_lower in ["badge", "word", "pop"]:
 
 
164
  params["fontcolor"] = "yellow"
165
  params["borderw"] = 0
166
 
167
  escaped_word = word.replace(':', FFMPEG_ESCAPE_CHAR + ':')
168
 
169
- # Filter für das einzelne, hervorgehobene Wort
170
  drawtext_filter = (
171
  f"drawtext=text='{escaped_word}':"
172
  f"fontcolor={params['fontcolor']}:"
173
  f"fontsize={params['fontsize_override']}:"
174
  f"borderw={params['borderw']}:"
175
  f"bordercolor={params['bordercolor']}:"
176
- + (f"box={params['box']}:boxcolor={params['boxcolor']}:boxborderw=10:" if params["box"] else "") +
177
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
178
  )
179
 
180
 
181
  if font_option:
182
- # font_option enthält bereits 'fontfile='
183
  drawtext_filter += f":{font_option}"
184
 
185
  # Der Highlight-Filter ist nur aktiv, wenn das Wort aktiv ist (via Alpha-Expression).
@@ -206,15 +206,13 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
206
  current_word_index = 0
207
  clips_with_text = []
208
 
209
- # NEU: Schriftart finden basierend auf der Auswahl
210
  font_path = get_font_path(selected_font)
211
 
212
  # Pfad für FFmpeg vorbereiten und maskieren.
213
  font_option = ""
214
  if font_path:
215
- # Ersetze eventuelle Backslashes in Pfaden (obwohl unwahrscheinlich unter Linux)
216
  escaped_font_path = str(font_path).replace(FFMPEG_ESCAPE_CHAR, FFMPEG_ESCAPE_CHAR + FFMPEG_ESCAPE_CHAR)
217
- # Behandelt das Doppelpunkt-Problem von FFmpeg in Pfaden (wichtig für Filtergraphen)
218
  escaped_font_path = escaped_font_path.replace(':', FFMPEG_ESCAPE_CHAR + ':')
219
  font_option = f"fontfile='{escaped_font_path}'"
220
 
@@ -241,25 +239,27 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
241
  drawtext_filters = []
242
 
243
  if full_text:
244
- # ERSTE SCHICHT: Der gesamte Satz (als STABILE BASIS mit Kasten)
245
  base_filter = create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, subtitle_style)
246
  drawtext_filters.append(base_filter)
247
 
248
- # ZWEITE SCHICHT: Highlight-Layer für jedes Wort
249
- word_start_time = 0.0
250
- for word in word_segment:
251
- highlight_filter = create_highlight_word_filter(
252
- word,
253
- full_text,
254
- word_start_time,
255
- duration_per_word,
256
- font_option,
257
- font_size,
258
- y_pos,
259
- subtitle_style
260
- )
261
- drawtext_filters.append(highlight_filter)
262
- word_start_time += duration_per_word
 
 
263
 
264
 
265
  # 3. Basis- und Fade-Filter
@@ -275,7 +275,7 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
275
 
276
  # 4. Kombiniere alle Filter
277
  if drawtext_filters:
278
- # Wichtig: Der Basis-Satz muss als erster Filter, die Highlights als letzte Filter stehen.
279
  all_drawtext_filters = ",".join(drawtext_filters)
280
  vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
281
  else:
@@ -291,11 +291,9 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
291
  ]
292
 
293
  try:
294
- # Hinzufügen von stdout/stderr Erfassung für bessere Fehlerprotokollierung
295
  subprocess.run(cmd, check=True, capture_output=True, text=True)
296
  clips_with_text.append(clip_path)
297
  except subprocess.CalledProcessError as e:
298
- # Bereinigung bei Fehler
299
  shutil.rmtree(temp_dir)
300
  if audio_temp_dir: shutil.rmtree(audio_temp_dir)
301
  return None, f"❌ FFmpeg Fehler bei Bild {i+1}:\n{e.stderr}"
@@ -370,11 +368,11 @@ with gr.Blocks() as demo:
370
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)", scale=1)
371
  ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)", scale=2)
372
 
373
- # Untertitel-Stile
374
  subtitle_style_input = gr.Dropdown(
375
- ["Modern", "Pop", "Bold", "Badge", "Word"],
376
  label="Untertitel-Stil",
377
- value="Modern",
378
  interactive=True,
379
  scale=1
380
  )
@@ -397,9 +395,9 @@ with gr.Blocks() as demo:
397
  ypos_input,
398
  audio_input,
399
  subtitle_style_input,
400
- font_select_input # NEUER Input
401
  ],
402
  outputs=[out_video, status]
403
  )
404
 
405
- demo.launch()
 
28
  def get_font_path(font_name):
29
  """
30
  Gibt den tatsächlichen, existierenden Pfad für die ausgewählte Schriftart zurück.
 
31
  """
32
  requested_path = FONT_MAP.get(font_name)
33
 
 
34
  if requested_path is None or os.path.exists(requested_path):
35
  return requested_path
36
 
 
37
  for name, path in FONT_MAP.items():
38
  if path and os.path.exists(path):
39
  print(f"Warnung: Ausgewählte Schriftart '{font_name}' nicht gefunden. Verwende Fallback: '{name}'")
40
  return path
41
 
 
42
  print("Warnung: Keine bevorzugten Schriftarten gefunden. Verwende FFmpeg System Standard.")
43
  return None
44
 
 
66
 
67
  def create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, style):
68
  """
69
+ Erstellt den FFmpeg drawtext Filter für die Basisschicht (den gesamten Satzabschnitt).
70
+ Dies ist entweder der gesamte Satz oder die Box für statischen Text.
71
  """
 
72
  base_params = {
73
  "fontcolor": "white",
74
+ "borderw": 2, # Standard Schatten/Rand
75
  "bordercolor": "black",
76
+ "box": 1, "boxcolor": "[email protected]", # Semi-transparente schwarze Box
77
  "fontsize": font_size
78
  }
79
 
80
  style_lower = style.lower()
81
 
82
+ if style_lower == "highlight":
83
+ # Hervorheben: Der gesamte Satz als Basis, aber nur mit leichtem Schatten
84
+ base_params["box"] = 0
85
+ base_params["borderw"] = 2
86
+
87
+ elif style_lower == "static":
88
+ # Statisch: Der gesamte Satz in einer Box, keine Animation, bleibt die ganze Zeit sichtbar
 
 
 
89
  base_params["box"] = 1
90
+ base_params["borderw"] = 0
 
91
 
92
+ elif style_lower == "dynamic":
93
+ # Dynamisch: Große Schrift, leichte Box, wird später vom Highlight überlagert
94
+ base_params["box"] = 1
95
+ base_params["boxcolor"] = "[email protected]"
96
+ base_params["borderw"] = 0
97
+ base_params["fontsize"] = font_size * 1.2
98
+
99
 
100
  escaped_text = full_text.replace(':', FFMPEG_ESCAPE_CHAR + ':')
101
 
102
+ # Filter für den gesamten Satz
103
  drawtext_filter = (
104
  f"drawtext=text='{escaped_text}':"
105
  f"fontcolor={base_params['fontcolor']}:"
 
111
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
112
  )
113
 
 
114
  if font_option:
 
115
  drawtext_filter += f":{font_option}"
116
 
117
+ # Der statische Stil wird sofort und für die gesamte Clip-Dauer eingeblendet
118
+ if style_lower == "static":
119
+ drawtext_filter += f":enable='between(t, 0, {duration_clip})'"
120
+
121
+ # Für Highlight und Dynamic brauchen wir die Basis als konstante Referenz
122
+ else:
123
+ # Bei "Highlight" und "Dynamic" ist dies der Basis-Text, der IMMER sichtbar ist.
124
+ drawtext_filter += f":enable='between(t, 0, {duration_clip})'"
125
+
126
  return drawtext_filter
127
 
128
 
129
  def create_highlight_word_filter(word, full_text, start_time, duration, font_option, font_size, y_pos, style):
130
  """
131
+ Erstellt den FFmpeg drawtext Filter für die Highlight-Schicht (nur das aktive Wort),
132
+ es sei denn, der Stil ist 'Static'.
133
  """
134
+ # Wenn statisch, wird kein Highlight benötigt
135
+ if style.lower() == "static":
136
+ return None
137
+
138
  word_end_time = start_time + duration
139
 
140
  # Alpha-Ausdruck für smooth Fade-In und Fade-Out der HIGHLIGHT-FARBE
 
150
  "fontcolor": "yellow",
151
  "borderw": 0,
152
  "bordercolor": "black",
 
153
  "fontsize_override": font_size * 1.05 # Leicht vergrößert
154
  }
155
 
156
  style_lower = style.lower()
157
 
158
+ if style_lower == "dynamic":
159
+ # Dynamisch: Schrift deutlich größer und mit Rand, zentriert.
 
 
 
 
 
160
  params["fontcolor"] = "yellow"
161
  params["borderw"] = 4
162
+ params["fontsize_override"] = font_size * 1.5
163
+
164
+ else: # Highlight
165
+ # Highlight: Gelbe Schrift, kein Rand
166
  params["fontcolor"] = "yellow"
167
  params["borderw"] = 0
168
 
169
  escaped_word = word.replace(':', FFMPEG_ESCAPE_CHAR + ':')
170
 
171
+ # Filter für das einzelne, hervorgehobene Wort (Das gesamte Wort wird gezeichnet)
172
  drawtext_filter = (
173
  f"drawtext=text='{escaped_word}':"
174
  f"fontcolor={params['fontcolor']}:"
175
  f"fontsize={params['fontsize_override']}:"
176
  f"borderw={params['borderw']}:"
177
  f"bordercolor={params['bordercolor']}:"
 
178
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
179
  )
180
 
181
 
182
  if font_option:
 
183
  drawtext_filter += f":{font_option}"
184
 
185
  # Der Highlight-Filter ist nur aktiv, wenn das Wort aktiv ist (via Alpha-Expression).
 
206
  current_word_index = 0
207
  clips_with_text = []
208
 
209
+ # Schriftart finden basierend auf der Auswahl
210
  font_path = get_font_path(selected_font)
211
 
212
  # Pfad für FFmpeg vorbereiten und maskieren.
213
  font_option = ""
214
  if font_path:
 
215
  escaped_font_path = str(font_path).replace(FFMPEG_ESCAPE_CHAR, FFMPEG_ESCAPE_CHAR + FFMPEG_ESCAPE_CHAR)
 
216
  escaped_font_path = escaped_font_path.replace(':', FFMPEG_ESCAPE_CHAR + ':')
217
  font_option = f"fontfile='{escaped_font_path}'"
218
 
 
239
  drawtext_filters = []
240
 
241
  if full_text:
242
+ # ERSTE SCHICHT: Der gesamte Satz (als STABILE BASIS oder STATISCHE BOX)
243
  base_filter = create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, subtitle_style)
244
  drawtext_filters.append(base_filter)
245
 
246
+ # ZWEITE SCHICHT: Highlight-Layer (nur wenn nicht "Static")
247
+ if subtitle_style.lower() != "static":
248
+ word_start_time = 0.0
249
+ for word in word_segment:
250
+ highlight_filter = create_highlight_word_filter(
251
+ word,
252
+ full_text,
253
+ word_start_time,
254
+ duration_per_word,
255
+ font_option,
256
+ font_size,
257
+ y_pos,
258
+ subtitle_style
259
+ )
260
+ if highlight_filter:
261
+ drawtext_filters.append(highlight_filter)
262
+ word_start_time += duration_per_word
263
 
264
 
265
  # 3. Basis- und Fade-Filter
 
275
 
276
  # 4. Kombiniere alle Filter
277
  if drawtext_filters:
278
+ # Die Reihenfolge ist wichtig: Basis zuerst, Highlights zuletzt
279
  all_drawtext_filters = ",".join(drawtext_filters)
280
  vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
281
  else:
 
291
  ]
292
 
293
  try:
 
294
  subprocess.run(cmd, check=True, capture_output=True, text=True)
295
  clips_with_text.append(clip_path)
296
  except subprocess.CalledProcessError as e:
 
297
  shutil.rmtree(temp_dir)
298
  if audio_temp_dir: shutil.rmtree(audio_temp_dir)
299
  return None, f"❌ FFmpeg Fehler bei Bild {i+1}:\n{e.stderr}"
 
368
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)", scale=1)
369
  ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)", scale=2)
370
 
371
+ # NEU: Reduzierte Untertitel-Stile
372
  subtitle_style_input = gr.Dropdown(
373
+ ["Highlight", "Dynamic", "Static"],
374
  label="Untertitel-Stil",
375
+ value="Highlight",
376
  interactive=True,
377
  scale=1
378
  )
 
395
  ypos_input,
396
  audio_input,
397
  subtitle_style_input,
398
+ font_select_input
399
  ],
400
  outputs=[out_video, status]
401
  )
402
 
403
+ demo.launch(