Tim13ekd commited on
Commit
029da5b
·
verified ·
1 Parent(s): fb76288

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -81
app.py CHANGED
@@ -6,7 +6,7 @@ import subprocess
6
  import shutil
7
  import os
8
 
9
- # NEU: Dauer des Fade-In/Out für jedes einzelne Wort (z.B. 0.2 Sekunden)
10
  WORD_FADE_DURATION = 0.2
11
  FFMPEG_ESCAPE_CHAR = "\\"
12
 
@@ -24,32 +24,22 @@ def get_font_path():
24
  for font in possible_fonts:
25
  if os.path.exists(font):
26
  return font
27
- return None # Fallback: FFmpeg soll selbst suchen (klappt manchmal nicht)
28
 
29
  def save_temp_audio(audio_file_path):
30
- """
31
- Speichert die hochgeladene Audio-Datei in einem temporären Verzeichnis.
32
- Erwartet einen Dateipfad-String von Gradio.
33
- """
34
  if not audio_file_path:
35
  return None, None
36
-
37
- # Gradio liefert einen String-Pfad zum temporären Speicherort
38
  input_path = Path(audio_file_path)
39
-
40
- # Bestimme die Erweiterung
41
  ext = input_path.suffix
42
  if ext.lower() not in allowed_audios:
43
  ext = ".mp3"
44
 
45
- # Erstelle das Zielverzeichnis und den Zielpfad
46
  temp_audio_dir = Path(tempfile.mkdtemp())
47
  temp_audio = temp_audio_dir / f"input{ext}"
48
 
49
- # Kopiere die Datei vom Gradio-Temp-Pfad in unseren eigenen Temp-Pfad
50
  try:
51
  shutil.copyfile(input_path, temp_audio)
52
- # Rückgabe des Verzeichnisses, das später gelöscht werden kann, und des Dateipfads
53
  return temp_audio_dir, temp_audio
54
  except Exception as e:
55
  print(f"Fehler beim Kopieren der Audiodatei: {e}")
@@ -57,86 +47,118 @@ def save_temp_audio(audio_file_path):
57
  shutil.rmtree(temp_audio_dir)
58
  return None, None
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- def create_timed_drawtext(word, start_time, duration, font_option, font_size, y_pos, style):
62
- """Erstellt einen FFmpeg drawtext Filter, der ein Wort mit weichen Übergängen (Alpha-Kanal) einblendet,
63
- basierend auf dem gewählten Stil."""
64
- global FFMPEG_ESCAPE_CHAR
65
- global WORD_FADE_DURATION
66
-
67
- # 1. Escaping: Ersetze alle ":" durch "\:" für FFmpeg
68
- escaped_word = word.replace(':', f"{FFMPEG_ESCAPE_CHAR}:")
69
 
70
- # Definiere die Start- und Endzeit des WORTES
71
- end_time = start_time + duration
 
 
 
 
 
 
72
 
73
- # Zeitpunkte für den Fade
74
- fade_in_end = start_time + WORD_FADE_DURATION
75
- fade_out_start = end_time - WORD_FADE_DURATION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  # Alpha-Ausdruck für smooth Fade-In und Fade-Out
78
  alpha_expression = (
79
  f"if(lt(t,{start_time}), 0, "
80
- f"if(lt(t,{fade_in_end}), (t-{start_time})/{WORD_FADE_DURATION}, "
81
- f"if(lt(t,{fade_out_start}), 1, "
82
- f"if(lt(t,{end_time}), ({end_time}-t)/{WORD_FADE_DURATION}, 0))))"
83
  )
84
-
85
- # --- STYLING BASIEREND AUF AUSWAHL (Style-Namen werden in Kleinbuchstaben übergeben) ---
86
-
87
- # Default-Werte (Modern-Stil)
88
  params = {
89
- "fontcolor": "white",
90
- "borderw": 2,
91
  "bordercolor": "black",
92
- "box": 0,
93
- "boxcolor": "",
94
  "fontsize_override": font_size
95
  }
 
 
 
 
 
 
 
 
96
 
97
- style_lower = style.lower().replace(" ", "")
98
-
99
- if style_lower == "pop":
100
- # Heller, auffälliger Text (Gelb mit Kontur, etwas größer)
101
  params["fontcolor"] = "yellow"
102
- params["borderw"] = 3
103
  params["fontsize_override"] = font_size * 1.1
104
 
105
- elif style_lower == "bold":
106
- # Starker Kontrast, sehr dickerer Rand
107
- params["fontcolor"] = "white"
108
- params["borderw"] = 5 # Dickerer Rand für "Bold"
109
- params["fontsize_override"] = font_size * 1.05
110
 
111
  elif style_lower == "badge":
112
- # Grüner Kasten (Mint) als Hintergrund für das aktive Wort
113
  params["fontcolor"] = "white"
114
  params["borderw"] = 0
115
  params["box"] = 1
116
- # Mint Green (0x50C878) @1.0 (opak)
117
- params["boxcolor"] = "0x50C878@1.0"
118
 
119
  elif style_lower == "word":
120
- # Gelber Kasten als Hintergrund für das aktive Wort
121
- params["fontcolor"] = "white"
122
  params["borderw"] = 0
123
  params["box"] = 1
124
- # Gold/Yellow (0xFFD700) @1.0 (opak)
125
- params["boxcolor"] = "0xFFD700@1.0"
126
 
127
- # Filter-String basierend auf den dynamischen Parametern erstellen
128
- drawtext_filter = (
129
- f"drawtext=text='{escaped_word}'{font_option}:"
 
 
130
  f"fontcolor={params['fontcolor']}:"
131
  f"fontsize={params['fontsize_override']}:"
132
  f"borderw={params['borderw']}:"
133
  f"bordercolor={params['bordercolor']}:"
134
- # Füge Box-Parameter nur hinzu, wenn box=1 (Badge- oder Word-Stil)
135
  + (f"box={params['box']}:boxcolor={params['boxcolor']}:boxborderw=10:" if params["box"] else "") +
136
- f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:"
137
  f"alpha='{alpha_expression}'"
138
  )
139
- return drawtext_filter
140
 
141
 
142
  def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file, subtitle_style):
@@ -156,7 +178,7 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
156
  remainder = total_words % num_images
157
 
158
  current_word_index = 0
159
- clips_with_text = [] # Paths der generierten MP4-Clips
160
 
161
  # Schriftart finden
162
  font_path = get_font_path()
@@ -173,26 +195,31 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
173
 
174
  # 1. Bestimme das Wortsegment für diesen Clip
175
  words_on_this_clip = base_words_per_clip + (1 if i < remainder else 0)
176
-
177
- # Extrahieren des Segments aus der Gesamtliste der Wörter
178
  word_segment = words[current_word_index : current_word_index + words_on_this_clip]
179
  current_word_index += len(word_segment)
180
 
 
 
181
  # 2. Berechne die Clip-Dauer
182
  text_duration = len(word_segment) * duration_per_word
183
- # Die Dauer ist das Maximum aus der gewünschten Bilddauer und der benötigten Textdauer
184
  duration_clip = max(duration_per_image, text_duration)
185
 
186
- # 3. Generiere Drawtext Filter (Startzeit ist relativ zum Clip-Start, also 0)
187
  drawtext_filters = []
188
- word_start_time = 0.0
189
- for word in word_segment:
190
- # Füge den Stil-Parameter hinzu
191
- filter_str = create_timed_drawtext(word, word_start_time, duration_per_word, font_option, font_size, y_pos, subtitle_style)
192
- drawtext_filters.append(filter_str)
193
- word_start_time += duration_per_word
194
-
195
- # 4. Basis- und Fade-Filter
 
 
 
 
 
 
 
196
  base_filters = (
197
  "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
198
  "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
@@ -203,7 +230,7 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
203
  if fade_out_start < 0: fade_out_start = 0
204
  fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={fade_out_start}:d={fade_duration}"
205
 
206
- # 5. Kombiniere alle Filter
207
  if drawtext_filters:
208
  all_drawtext_filters = ",".join(drawtext_filters)
209
  vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
@@ -211,7 +238,7 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
211
  # Kein Text mehr: Nur Bild mit Fade
212
  vf_filters_clip = f"{base_filters},{fade_img_filter}"
213
 
214
- # 6. FFmpeg Command zum Erstellen des Clips
215
  cmd = [
216
  "ffmpeg", "-y", "-loop", "1", "-i", str(img_path),
217
  "-t", str(duration_clip),
@@ -280,18 +307,18 @@ with gr.Blocks() as demo:
280
 
281
  with gr.Row():
282
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
283
- text_input = gr.Textbox(label="Text (Wörter werden gleichmäßig auf alle Bilder verteilt)", lines=5, placeholder="Jedes Wort wird für 'Dauer pro Wort' angezeigt.")
284
 
285
  with gr.Row():
286
  duration_image_input = gr.Number(value=3, label="Mindest-Dauer pro BILD (s)")
287
- duration_word_input = gr.Number(value=1.0, label="Dauer pro WORT (s) [bestimmt Geschwindigkeit der Text-Anzeige]")
288
  fade_input = gr.Number(value=0.5, label="Bild-Fade Dauer (s)")
289
 
290
  with gr.Row():
291
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
292
  ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)")
293
 
294
- # NEUE EINGABE FÜR STILE: Enthält jetzt "Word" und nutzt korrekte Groß-/Kleinschreibung
295
  subtitle_style_input = gr.Dropdown(
296
  ["Modern", "Pop", "Bold", "Badge", "Word"],
297
  label="Untertitel-Stil",
@@ -305,8 +332,6 @@ with gr.Blocks() as demo:
305
  out_video = gr.Video(label="Ergebnis")
306
  status = gr.Textbox(label="Status")
307
 
308
- # KORREKTE REIHENFOLGE DER INPUTS aktualisiert um 'subtitle_style_input':
309
- # (images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file, subtitle_style)
310
  btn.click(
311
  fn=generate_slideshow_with_audio,
312
  inputs=[
@@ -318,7 +343,7 @@ with gr.Blocks() as demo:
318
  font_size_input,
319
  ypos_input,
320
  audio_input,
321
- subtitle_style_input # NEUE EINGABE
322
  ],
323
  outputs=[out_video, status]
324
  )
 
6
  import shutil
7
  import os
8
 
9
+ # Konstanten
10
  WORD_FADE_DURATION = 0.2
11
  FFMPEG_ESCAPE_CHAR = "\\"
12
 
 
24
  for font in possible_fonts:
25
  if os.path.exists(font):
26
  return font
27
+ return None
28
 
29
  def save_temp_audio(audio_file_path):
30
+ """Speichert die hochgeladene Audio-Datei in einem temporären Verzeichnis."""
 
 
 
31
  if not audio_file_path:
32
  return None, None
 
 
33
  input_path = Path(audio_file_path)
 
 
34
  ext = input_path.suffix
35
  if ext.lower() not in allowed_audios:
36
  ext = ".mp3"
37
 
 
38
  temp_audio_dir = Path(tempfile.mkdtemp())
39
  temp_audio = temp_audio_dir / f"input{ext}"
40
 
 
41
  try:
42
  shutil.copyfile(input_path, temp_audio)
 
43
  return temp_audio_dir, temp_audio
44
  except Exception as e:
45
  print(f"Fehler beim Kopieren der Audiodatei: {e}")
 
47
  shutil.rmtree(temp_audio_dir)
48
  return None, None
49
 
50
+ def create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, style):
51
+ """
52
+ Erstellt den FFmpeg drawtext Filter für die Basisschicht (den gesamten Satzabschnitt),
53
+ der für die volle Clip-Dauer sichtbar ist.
54
+ """
55
+ # Standard-Stil für inaktiven Text (Modern)
56
+ base_params = {
57
+ "fontcolor": "white",
58
+ "borderw": 2,
59
+ "bordercolor": "black",
60
+ "box": 0, "boxcolor": "",
61
+ "fontsize": font_size
62
+ }
63
 
64
+ style_lower = style.lower()
 
 
 
 
 
 
 
65
 
66
+ # SPEZIALFALL: Pop Style (IMG_1456)
67
+ if style_lower == "pop":
68
+ # Ganzer Satzabschnitt in dunkler, semi-transparenter Box
69
+ base_params["box"] = 1
70
+ base_params["boxcolor"] = "[email protected]"
71
+ base_params["fontsize"] = font_size * 1.1
72
+
73
+ escaped_text = full_text.replace(':', FFMPEG_ESCAPE_CHAR + ':')
74
 
75
+ # Filter für den gesamten Satz, sichtbar für die gesamte Clip-Dauer
76
+ return (
77
+ f"drawtext=text='{escaped_text}':"
78
+ f"fontcolor={base_params['fontcolor']}:"
79
+ f"fontsize={base_params['fontsize']}:"
80
+ f"borderw={base_params['borderw']}:"
81
+ f"bordercolor={base_params['bordercolor']}:"
82
+ + (f"box={base_params['box']}:boxcolor={base_params['boxcolor']}:boxborderw=10:" if base_params["box"] else "") +
83
+ f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:{font_option}:"
84
+ f"enable='between(t, 0, {duration_clip})'"
85
+ )
86
+
87
+
88
+ def create_highlight_word_filter(word, full_text, start_time, duration, font_option, font_size, y_pos, style):
89
+ """
90
+ Erstellt den FFmpeg drawtext Filter für die Highlight-Schicht (nur das aktive Wort).
91
+ Dieses Wort wird in der Mitte des Bildschirms angezeigt und überblendet die Basisschicht
92
+ während seiner Aktivierungszeit.
93
+ """
94
+ word_end_time = start_time + duration
95
 
96
  # Alpha-Ausdruck für smooth Fade-In und Fade-Out
97
  alpha_expression = (
98
  f"if(lt(t,{start_time}), 0, "
99
+ f"if(lt(t,{start_time + WORD_FADE_DURATION}), (t-{start_time})/{WORD_FADE_DURATION}, "
100
+ f"if(lt(t,{word_end_time - WORD_FADE_DURATION}), 1, "
101
+ f"if(lt(t,{word_end_time}), ({word_end_time}-t)/{WORD_FADE_DURATION}, 0))))"
102
  )
103
+
104
+ # Styling Parameter (Standard: Gelb/Bold-Highlight)
 
 
105
  params = {
106
+ "fontcolor": "yellow",
107
+ "borderw": 3,
108
  "bordercolor": "black",
109
+ "box": 0, "boxcolor": "",
 
110
  "fontsize_override": font_size
111
  }
112
+
113
+ style_lower = style.lower()
114
+
115
+ if style_lower == "modern":
116
+ # Modern: Minimaler Highlight (etwas größer)
117
+ params["fontcolor"] = "white"
118
+ params["borderw"] = 2
119
+ params["fontsize_override"] = font_size * 1.05
120
 
121
+ elif style_lower == "bold":
122
+ # Bold: Gelb mit starkem Rand (wie in IMG_1455)
 
 
123
  params["fontcolor"] = "yellow"
124
+ params["borderw"] = 4
125
  params["fontsize_override"] = font_size * 1.1
126
 
127
+ elif style_lower == "pop":
128
+ # Pop: Gelbe Schrift, kein Rand (Box wird von der Basisschicht gezeichnet)
129
+ params["fontcolor"] = "yellow"
130
+ params["borderw"] = 0
131
+ params["fontsize_override"] = font_size * 1.1
132
 
133
  elif style_lower == "badge":
134
+ # Badge: Mint Green Box um das Wort (wie in IMG_1453)
135
  params["fontcolor"] = "white"
136
  params["borderw"] = 0
137
  params["box"] = 1
138
+ params["boxcolor"] = "[email protected]" # Mint Green
139
+ params["fontsize_override"] = font_size * 1.05
140
 
141
  elif style_lower == "word":
142
+ # Word: Gelbe Box um das Wort (wie in IMG_1454)
143
+ params["fontcolor"] = "black" # Textfarbe auf schwarz, da Box gelb ist
144
  params["borderw"] = 0
145
  params["box"] = 1
146
+ params["boxcolor"] = "[email protected]" # Yellow
147
+ params["fontsize_override"] = font_size * 1.05
148
 
149
+ escaped_word = word.replace(':', FFMPEG_ESCAPE_CHAR + ':')
150
+
151
+ # Filter für das einzelne, hervorgehobene Wort
152
+ return (
153
+ f"drawtext=text='{escaped_word}':"
154
  f"fontcolor={params['fontcolor']}:"
155
  f"fontsize={params['fontsize_override']}:"
156
  f"borderw={params['borderw']}:"
157
  f"bordercolor={params['bordercolor']}:"
 
158
  + (f"box={params['box']}:boxcolor={params['boxcolor']}:boxborderw=10:" if params["box"] else "") +
159
+ f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:{font_option}:"
160
  f"alpha='{alpha_expression}'"
161
  )
 
162
 
163
 
164
  def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file, subtitle_style):
 
178
  remainder = total_words % num_images
179
 
180
  current_word_index = 0
181
+ clips_with_text = []
182
 
183
  # Schriftart finden
184
  font_path = get_font_path()
 
195
 
196
  # 1. Bestimme das Wortsegment für diesen Clip
197
  words_on_this_clip = base_words_per_clip + (1 if i < remainder else 0)
 
 
198
  word_segment = words[current_word_index : current_word_index + words_on_this_clip]
199
  current_word_index += len(word_segment)
200
 
201
+ full_text = " ".join(word_segment)
202
+
203
  # 2. Berechne die Clip-Dauer
204
  text_duration = len(word_segment) * duration_per_word
 
205
  duration_clip = max(duration_per_image, text_duration)
206
 
 
207
  drawtext_filters = []
208
+
209
+ if full_text:
210
+ # ERSTE SCHICHT: Der gesamte Satz (als Basis)
211
+ base_filter = create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, subtitle_style)
212
+ drawtext_filters.append(base_filter)
213
+
214
+ # ZWEITE SCHICHT: Highlight-Layer für jedes Wort
215
+ word_start_time = 0.0
216
+ for word in word_segment:
217
+ highlight_filter = create_highlight_word_filter(word, full_text, word_start_time, duration_per_word, font_option, font_size, y_pos, subtitle_style)
218
+ drawtext_filters.append(highlight_filter)
219
+ word_start_time += duration_per_word
220
+
221
+
222
+ # 3. Basis- und Fade-Filter
223
  base_filters = (
224
  "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
225
  "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
 
230
  if fade_out_start < 0: fade_out_start = 0
231
  fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={fade_out_start}:d={fade_duration}"
232
 
233
+ # 4. Kombiniere alle Filter
234
  if drawtext_filters:
235
  all_drawtext_filters = ",".join(drawtext_filters)
236
  vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
 
238
  # Kein Text mehr: Nur Bild mit Fade
239
  vf_filters_clip = f"{base_filters},{fade_img_filter}"
240
 
241
+ # 5. FFmpeg Command zum Erstellen des Clips
242
  cmd = [
243
  "ffmpeg", "-y", "-loop", "1", "-i", str(img_path),
244
  "-t", str(duration_clip),
 
307
 
308
  with gr.Row():
309
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
310
+ text_input = gr.Textbox(label="Text (Wörter werden gleichmäßig auf alle Bilder verteilt)", lines=5, placeholder="Der Satzabschnitt ist pro Clip sichtbar. Das aktive Wort wird hervorgehoben.")
311
 
312
  with gr.Row():
313
  duration_image_input = gr.Number(value=3, label="Mindest-Dauer pro BILD (s)")
314
+ duration_word_input = gr.Number(value=1.0, label="Dauer pro WORT (s) [bestimmt Geschwindigkeit der Hervorhebung]")
315
  fade_input = gr.Number(value=0.5, label="Bild-Fade Dauer (s)")
316
 
317
  with gr.Row():
318
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
319
  ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)")
320
 
321
+ # Untertitel-Stile
322
  subtitle_style_input = gr.Dropdown(
323
  ["Modern", "Pop", "Bold", "Badge", "Word"],
324
  label="Untertitel-Stil",
 
332
  out_video = gr.Video(label="Ergebnis")
333
  status = gr.Textbox(label="Status")
334
 
 
 
335
  btn.click(
336
  fn=generate_slideshow_with_audio,
337
  inputs=[
 
343
  font_size_input,
344
  ypos_input,
345
  audio_input,
346
+ subtitle_style_input
347
  ],
348
  outputs=[out_video, status]
349
  )