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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -72
app.py CHANGED
@@ -47,15 +47,15 @@ def save_temp_audio(audio_file_path):
47
  shutil.rmtree(temp_audio_dir)
48
  return None, None
49
 
50
- def create_cumulative_base_filter(text_to_draw, start_time, font_option, font_size, y_pos, style):
51
  """
52
- Erstellt den FFmpeg drawtext Filter für die Basisschicht des kumulierten Textes.
53
- Dieser Text bleibt ab start_time bis zum Ende des Clips sichtbar.
54
  """
55
  # Standard-Stil
56
  base_params = {
57
  "fontcolor": "white",
58
- "borderw": 0,
59
  "bordercolor": "black",
60
  "box": 0, "boxcolor": "",
61
  "fontsize": font_size
@@ -63,111 +63,120 @@ def create_cumulative_base_filter(text_to_draw, start_time, font_option, font_si
63
 
64
  style_lower = style.lower()
65
 
66
- # --- STYLES FÜR DIE BASISSCHICHT (Der Satz selbst) ---
67
  if style_lower == "modern":
68
- # Modern: Graue, semi-transparente Hintergrundbox (HINWEIS: FFmpeg unterstützt keine abgerundeten Ecken)
69
  base_params["box"] = 1
70
- base_params["boxcolor"] = "0x444444@0.6" # Dunkelgrau mit 60% Transparenz
71
- base_params["fontcolor"] = "white"
72
- base_params["borderw"] = 0
73
  base_params["fontsize"] = font_size
74
 
 
75
  elif style_lower == "pop":
76
- # Pop: Schwarze, semi-transparente Hintergrundbox
77
  base_params["box"] = 1
78
  base_params["boxcolor"] = "[email protected]"
79
  base_params["fontsize"] = font_size * 1.1
80
- base_params["borderw"] = 0
81
 
82
- # Für andere Stile wird die Basisschicht ohne Box oder Rand gezeichnet (falls sie überhaupt gebraucht wird)
 
 
83
 
84
- escaped_text = text_to_draw.replace(':', FFMPEG_ESCAPE_CHAR + ':')
85
 
86
- # Filter für den gesamten Satz, der ab start_time sichtbar wird
87
  drawtext_filter = (
88
  f"drawtext=text='{escaped_text}':"
89
  f"fontcolor={base_params['fontcolor']}:"
90
  f"fontsize={base_params['fontsize']}:"
91
  f"borderw={base_params['borderw']}:"
92
  f"bordercolor={base_params['bordercolor']}:"
93
- # boxborderw=10 fügt Polsterung hinzu
94
  + (f"box={base_params['box']}:boxcolor={base_params['boxcolor']}:boxborderw=10:" if base_params["box"] else "") +
95
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
96
  )
97
 
 
98
  if font_option:
99
  drawtext_filter += f":{font_option}"
100
 
101
- # enable='gt(t, {start_time})' sorgt dafür, dass dieser Text dauerhaft ab start_time angezeigt wird
102
- drawtext_filter += f":enable='gt(t, {start_time - 0.05})'" # -0.05 für nahtlosen Übergang
103
  return drawtext_filter
104
 
105
 
106
- def create_highlight_word_filter(word, 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
  """
110
  word_end_time = start_time + duration
111
 
112
- # Alpha-Ausdruck für smooth Fade-In und Fade-Out
113
- alpha_expression = (
114
  f"if(lt(t,{start_time}), 0, "
115
  f"if(lt(t,{start_time + WORD_FADE_DURATION}), (t-{start_time})/{WORD_FADE_DURATION}, "
116
  f"if(lt(t,{word_end_time - WORD_FADE_DURATION}), 1, "
117
  f"if(lt(t,{word_end_time}), ({word_end_time}-t)/{WORD_FADE_DURATION}, 0))))"
118
  )
119
 
120
- # Styling Parameter (Standard: Gelb/Bold-Highlight)
121
  params = {
122
  "fontcolor": "yellow",
123
- "borderw": 3,
124
  "bordercolor": "black",
125
  "box": 0, "boxcolor": "",
126
- "fontsize_override": font_size
127
  }
128
 
129
  style_lower = style.lower()
130
 
131
- # --- STYLES FÜR DIE HIGHLIGHT-SCHICHT (Das aktuell hervorgehobene Wort) ---
132
  if style_lower == "modern":
133
- # Modern: Gelbe Schrift über dem Basissatz
134
  params["fontcolor"] = "yellow"
135
  params["borderw"] = 0
136
- params["fontsize_override"] = font_size * 1.05
137
 
138
  elif style_lower == "bold":
139
  # Bold: Gelb mit starkem Rand
140
  params["fontcolor"] = "yellow"
141
  params["borderw"] = 4
142
- params["fontsize_override"] = font_size * 1.1
143
 
144
- elif style_lower == "pop":
145
- # Pop: Gelbe Schrift, kein Rand
 
146
  params["fontcolor"] = "yellow"
147
  params["borderw"] = 0
148
- params["fontsize_override"] = font_size * 1.1
149
-
150
- elif style_lower == "badge":
151
- # Badge: Mint Green Box um das Wort
152
- params["fontcolor"] = "white"
153
- params["borderw"] = 0
154
- params["box"] = 1
155
- params["boxcolor"] = "[email protected]" # Mint Green
156
- params["fontsize_override"] = font_size * 1.05
157
 
158
- elif style_lower == "word":
159
- # Word: Gelbe Box um das Wort
160
- params["fontcolor"] = "black" # Textfarbe auf schwarz, da Box gelb ist
161
- params["borderw"] = 0
162
- params["box"] = 1
163
- params["boxcolor"] = "[email protected]" # Yellow
164
- params["fontsize_override"] = font_size * 1.05
 
 
 
 
 
 
 
 
165
 
166
- escaped_word = word.replace(':', FFMPEG_ESCAPE_CHAR + ':')
 
 
 
 
 
 
 
 
167
 
168
- # Filter für das einzelne, hervorgehobene Wort
169
  drawtext_filter = (
170
- f"drawtext=text='{escaped_word}':"
171
  f"fontcolor={params['fontcolor']}:"
172
  f"fontsize={params['fontsize_override']}:"
173
  f"borderw={params['borderw']}:"
@@ -176,10 +185,12 @@ def create_highlight_word_filter(word, start_time, duration, font_option, font_s
176
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
177
  )
178
 
 
179
  if font_option:
180
  drawtext_filter += f":{font_option}"
181
 
182
- drawtext_filter += f":alpha='{alpha_expression}'"
 
183
  return drawtext_filter
184
 
185
 
@@ -229,29 +240,17 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
229
  drawtext_filters = []
230
 
231
  if full_text:
232
- cumulative_text_list = []
233
- word_start_time = 0.0
 
234
 
235
- for j, word in enumerate(word_segment):
236
-
237
- # Aktualisiere den kumulierten Text
238
- cumulative_text_list.append(word)
239
- current_cumulative_text = " ".join(cumulative_text_list)
240
-
241
- # ERSTE SCHICHT: Kumulierter Basistext (wird ab diesem Wort permanent sichtbar)
242
- base_cumulative_filter = create_cumulative_base_filter(
243
- current_cumulative_text,
244
- word_start_time,
245
- font_option,
246
- font_size,
247
- y_pos,
248
- subtitle_style
249
- )
250
- drawtext_filters.append(base_cumulative_filter)
251
-
252
- # ZWEITE SCHICHT: Highlight-Layer (fadet ein und aus)
253
  highlight_filter = create_highlight_word_filter(
254
  word,
 
255
  word_start_time,
256
  duration_per_word,
257
  font_option,
@@ -260,7 +259,6 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
260
  subtitle_style
261
  )
262
  drawtext_filters.append(highlight_filter)
263
-
264
  word_start_time += duration_per_word
265
 
266
 
@@ -277,7 +275,7 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
277
 
278
  # 4. Kombiniere alle Filter
279
  if drawtext_filters:
280
- # Wichtig: Die Filter werden in der Reihenfolge angewendet, d.h. der letzte Filter liegt oben.
281
  all_drawtext_filters = ",".join(drawtext_filters)
282
  vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
283
  else:
@@ -354,7 +352,7 @@ with gr.Blocks() as demo:
354
 
355
  with gr.Row():
356
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
357
- text_input = gr.Textbox(label="Text (Wörter werden gleichmäßig auf alle Bilder verteilt)", lines=5, placeholder="Jedes Wort im Basissatz wird nach und nach hinzugefügt.")
358
 
359
  with gr.Row():
360
  duration_image_input = gr.Number(value=3, label="Mindest-Dauer pro BILD (s)")
 
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 (stabile Positionierung).
54
  """
55
  # Standard-Stil
56
  base_params = {
57
  "fontcolor": "white",
58
+ "borderw": 0, # Kein Rand
59
  "bordercolor": "black",
60
  "box": 0, "boxcolor": "",
61
  "fontsize": font_size
 
63
 
64
  style_lower = style.lower()
65
 
66
+ # SPEZIALFALL: Modern Style (graue, semi-transparente Hintergrundbox)
67
  if style_lower == "modern":
 
68
  base_params["box"] = 1
69
+ # Dunkelgrau (0x444444) mit 60% Transparenz (@0.6)
70
+ base_params["boxcolor"] = "[email protected]"
71
+ base_params["borderw"] = 0 # Kein Text-Rand bei Hintergrundbox
72
  base_params["fontsize"] = font_size
73
 
74
+ # SPEZIALFALL: Pop Style (schwarze Box)
75
  elif style_lower == "pop":
 
76
  base_params["box"] = 1
77
  base_params["boxcolor"] = "[email protected]"
78
  base_params["fontsize"] = font_size * 1.1
 
79
 
80
+ # Für andere Stile wird der Basistext nur als Schatten (borderw=2) gezeichnet
81
+ elif style_lower in ["bold", "badge", "word"]:
82
+ base_params["borderw"] = 2
83
 
84
+ escaped_text = full_text.replace(':', FFMPEG_ESCAPE_CHAR + ':')
85
 
86
+ # Filter für den gesamten Satz, sichtbar für die gesamte Clip-Dauer
87
  drawtext_filter = (
88
  f"drawtext=text='{escaped_text}':"
89
  f"fontcolor={base_params['fontcolor']}:"
90
  f"fontsize={base_params['fontsize']}:"
91
  f"borderw={base_params['borderw']}:"
92
  f"bordercolor={base_params['bordercolor']}:"
93
+ # boxborderw=10 fügt etwas Polsterung um die Box hinzu
94
  + (f"box={base_params['box']}:boxcolor={base_params['boxcolor']}:boxborderw=10:" if base_params["box"] else "") +
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})'"
 
103
  return drawtext_filter
104
 
105
 
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
 
115
+ # Alpha-Ausdruck für smooth Fade-In und Fade-Out der HIGHLIGHT-FARBE
116
+ highlight_alpha_expression = (
117
  f"if(lt(t,{start_time}), 0, "
118
  f"if(lt(t,{start_time + WORD_FADE_DURATION}), (t-{start_time})/{WORD_FADE_DURATION}, "
119
  f"if(lt(t,{word_end_time - WORD_FADE_DURATION}), 1, "
120
  f"if(lt(t,{word_end_time}), ({word_end_time}-t)/{WORD_FADE_DURATION}, 0))))"
121
  )
122
 
123
+ # Styling Parameter
124
  params = {
125
  "fontcolor": "yellow",
126
+ "borderw": 0,
127
  "bordercolor": "black",
128
  "box": 0, "boxcolor": "",
129
+ "fontsize_override": font_size * 1.05 # Leicht vergrößert
130
  }
131
 
132
  style_lower = style.lower()
133
 
 
134
  if style_lower == "modern":
135
+ # Modern: Gelbe Schrift, kein Rand
136
  params["fontcolor"] = "yellow"
137
  params["borderw"] = 0
 
138
 
139
  elif style_lower == "bold":
140
  # Bold: Gelb mit starkem Rand
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']}:"
 
185
  f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}"
186
  )
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
 
 
240
  drawtext_filters = []
241
 
242
  if full_text:
243
+ # ERSTE SCHICHT: Der gesamte Satz (als STABILE BASIS mit Kasten)
244
+ base_filter = create_sentence_base_filter(full_text, duration_clip, font_option, font_size, y_pos, subtitle_style)
245
+ drawtext_filters.append(base_filter)
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,
253
+ full_text,
254
  word_start_time,
255
  duration_per_word,
256
  font_option,
 
259
  subtitle_style
260
  )
261
  drawtext_filters.append(highlight_filter)
 
262
  word_start_time += duration_per_word
263
 
264
 
 
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:
 
352
 
353
  with gr.Row():
354
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
355
+ text_input = gr.Textbox(label="Text (Wörter werden gleichmäßig auf alle Bilder verteilt)", lines=5, placeholder="Der Basissatz wird konstant angezeigt. Das aktive Wort wird hervorgehoben.")
356
 
357
  with gr.Row():
358
  duration_image_input = gr.Number(value=3, label="Mindest-Dauer pro BILD (s)")