Tim13ekd commited on
Commit
4717f77
·
verified ·
1 Parent(s): 5d20d42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -54
app.py CHANGED
@@ -14,18 +14,39 @@ FFMPEG_ESCAPE_CHAR = "\\"
14
  allowed_medias = [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff"]
15
  allowed_audios = [".mp3", ".wav", ".m4a", ".ogg"]
16
 
17
- def get_font_path():
18
- """Versucht, eine Standard-Schriftart im Linux-System zu finden."""
19
- possible_fonts = [
20
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
21
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
22
- "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf"
23
- ]
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:
@@ -95,8 +116,9 @@ def create_sentence_base_filter(full_text, duration_clip, font_option, font_size
95
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
96
  )
97
 
98
- # Fügt fontfile nur hinzu, wenn vorhanden und vermeidet doppelte Doppelpunkte
99
  if font_option:
 
100
  drawtext_filter += f":{font_option}"
101
 
102
  drawtext_filter += f":enable='between(t, 0, {duration_clip})'"
@@ -106,9 +128,6 @@ def create_sentence_base_filter(full_text, duration_clip, font_option, font_size
106
  def create_highlight_word_filter(word, full_text, start_time, duration, font_option, font_size, y_pos, style):
107
  """
108
  Erstellt den FFmpeg drawtext Filter für die Highlight-Schicht (nur das aktive Wort).
109
- Da FFmpeg keine Wort-Positionen kennt, muss der GESAMTE Satz gezeichnet werden,
110
- aber nur das aktive Wort hat die Highlight-Farbe und der Rest ist transparent (alpha=0).
111
- Das ist notwendig, um die korrekte Zentrierung beizubehalten!
112
  """
113
  word_end_time = start_time + duration
114
 
@@ -141,42 +160,15 @@ def create_highlight_word_filter(word, full_text, start_time, duration, font_opt
141
  params["fontcolor"] = "yellow"
142
  params["borderw"] = 4
143
 
144
- # Hinweis: Badge/Word benötigen einen Trick, da FFmpeg keine Wort-Hintergrundboxen unterstützt.
145
- # Wir lassen sie hier auf den Standard-Highlight-Effekt fallen.
146
  elif style_lower in ["badge", "word", "pop"]:
147
  params["fontcolor"] = "yellow"
148
  params["borderw"] = 0
149
 
150
- escaped_text = full_text.replace(':', FFMPEG_ESCAPE_CHAR + ':')
151
 
152
- # Filter für das einzelne, hervorgehobene Wort (ACHTUNG: Es wird der gesamte Satz gezeichnet!)
153
- # Hier zeichnen wir nur das aktuell aktive Wort, was im Prinzip ein kumulierter Effekt ist,
154
- # aber ohne die Positionierungsfehler des vorherigen Versuchs, da nur EIN Wort gezeichnet wird.
155
  drawtext_filter = (
156
- f"drawtext=text='{word.replace(':', FFMPEG_ESCAPE_CHAR + ':')}':" # NUR das Wort
157
- f"fontcolor={params['fontcolor']}:"
158
- f"fontsize={params['fontsize_override']}:"
159
- f"borderw={params['borderw']}:"
160
- f"bordercolor={params['bordercolor']}:"
161
- # Die X-Position muss manuell berechnet werden, um es über dem Basistext zu positionieren.
162
- # Da wir das nicht können, zeichnen wir einfach den Satz und blenden das Highlight ein/aus.
163
- # NEUER ANSATZ: Wir zeichnen das Wort MIT SEINER EIGENEN ZENTRIERUNG und verlassen uns auf die Transparenz.
164
- # Das funktioniert nur, wenn das Highlight-Wort das gleiche ist wie der Basistext.
165
- # Da der Basistext in diesem stabilen Modell bereits den ganzen Satz anzeigt, müssen wir hier kreativ sein.
166
- # Wir müssen den highlight_alpha_expression verwenden, um das Wort EINZELN anzuzeigen.
167
-
168
- f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
169
- )
170
-
171
- # HACK: Da FFmpeg drawtext KEINE Wort-zu-Wort-Positions-Überlagerung unterstützt,
172
- # können wir nur das ZENTRIERTE WORT einblenden lassen.
173
- # Dies ist nicht perfekt, aber die stabilste Lösung.
174
-
175
- # Wir belassen es bei der Zentrierung des Worts selbst. Das wird visuell besser sein,
176
- # als wenn wir den ganzen Satz neu berechnen.
177
-
178
- drawtext_filter = (
179
- f"drawtext=text='{word.replace(':', FFMPEG_ESCAPE_CHAR + ':')}':"
180
  f"fontcolor={params['fontcolor']}:"
181
  f"fontsize={params['fontsize_override']}:"
182
  f"borderw={params['borderw']}:"
@@ -187,14 +179,15 @@ def create_highlight_word_filter(word, full_text, start_time, duration, font_opt
187
 
188
 
189
  if font_option:
 
190
  drawtext_filter += f":{font_option}"
191
 
192
- # Der Highlight-Filter ist nur aktiv, wenn das Wort aktiv ist.
193
  drawtext_filter += f":alpha='{highlight_alpha_expression}'"
194
  return drawtext_filter
195
 
196
 
197
- def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file, subtitle_style):
198
 
199
  if not images:
200
  return None, "❌ Keine Bilder ausgewählt"
@@ -213,9 +206,17 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
213
  current_word_index = 0
214
  clips_with_text = []
215
 
216
- # Schriftart finden
217
- font_path = get_font_path()
218
- font_option = f"fontfile='{font_path}'" if font_path else ""
 
 
 
 
 
 
 
 
219
 
220
  # Audio verarbeiten
221
  audio_temp_dir, temp_audio_file = save_temp_audio(audio_file) if audio_file else (None, None)
@@ -246,7 +247,6 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
246
 
247
  # ZWEITE SCHICHT: Highlight-Layer für jedes Wort
248
  word_start_time = 0.0
249
- # Wir verwenden hier NICHT den kumulativen Ansatz, sondern überlagern das Einzelwort
250
  for word in word_segment:
251
  highlight_filter = create_highlight_word_filter(
252
  word,
@@ -360,15 +360,23 @@ with gr.Blocks() as demo:
360
  fade_input = gr.Number(value=0.5, label="Bild-Fade Dauer (s)")
361
 
362
  with gr.Row():
363
- font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
364
- ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)")
 
 
 
 
 
 
 
365
 
366
  # Untertitel-Stile
367
  subtitle_style_input = gr.Dropdown(
368
  ["Modern", "Pop", "Bold", "Badge", "Word"],
369
  label="Untertitel-Stil",
370
  value="Modern",
371
- interactive=True
 
372
  )
373
 
374
  audio_input = gr.File(label="Audio (optional)", file_types=allowed_audios)
@@ -388,7 +396,8 @@ with gr.Blocks() as demo:
388
  font_size_input,
389
  ypos_input,
390
  audio_input,
391
- subtitle_style_input
 
392
  ],
393
  outputs=[out_video, status]
394
  )
 
14
  allowed_medias = [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff"]
15
  allowed_audios = [".mp3", ".wav", ".m4a", ".ogg"]
16
 
17
+ # Erweiterte Liste von Schriftpfaden, die in Hugging Face Spaces üblich sind
18
+ FONT_MAP = {
19
+ "System Default (FFmpeg)": None, # Kein fontfile-Parameter, FFmpeg wählt
20
+ "Noto Sans Bold": "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
21
+ "DejaVu Sans Bold": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
22
+ "Liberation Sans Bold": "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
23
+ "FreeSans Bold": "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
24
+ }
25
+ FONT_OPTIONS = list(FONT_MAP.keys())
26
+
27
+
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
 
49
+
50
  def save_temp_audio(audio_file_path):
51
  """Speichert die hochgeladene Audio-Datei in einem temporären Verzeichnis."""
52
  if not audio_file_path:
 
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})'"
 
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
 
 
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']}:"
 
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).
186
  drawtext_filter += f":alpha='{highlight_alpha_expression}'"
187
  return drawtext_filter
188
 
189
 
190
+ def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file, subtitle_style, selected_font):
191
 
192
  if not images:
193
  return None, "❌ Keine Bilder ausgewählt"
 
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
 
221
  # Audio verarbeiten
222
  audio_temp_dir, temp_audio_file = save_temp_audio(audio_file) if audio_file else (None, None)
 
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,
 
360
  fade_input = gr.Number(value=0.5, label="Bild-Fade Dauer (s)")
361
 
362
  with gr.Row():
363
+ font_select_input = gr.Dropdown(
364
+ FONT_OPTIONS,
365
+ label="Schriftart",
366
+ value="DejaVu Sans Bold" if "DejaVu Sans Bold" in FONT_OPTIONS else FONT_OPTIONS[0],
367
+ interactive=True,
368
+ scale=1
369
+ )
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
  )
381
 
382
  audio_input = gr.File(label="Audio (optional)", file_types=allowed_audios)
 
396
  font_size_input,
397
  ypos_input,
398
  audio_input,
399
+ subtitle_style_input,
400
+ font_select_input # NEUER Input
401
  ],
402
  outputs=[out_video, status]
403
  )