Tim13ekd commited on
Commit
1ce3011
·
verified ·
1 Parent(s): c5cfcb5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -47
app.py CHANGED
@@ -23,6 +23,7 @@ def get_font_path():
23
  return None # Fallback: FFmpeg soll selbst suchen (klappt manchmal nicht)
24
 
25
  def save_temp_audio(audio_file):
 
26
  if isinstance(audio_file, str):
27
  ext = Path(audio_file).suffix
28
  if ext.lower() not in allowed_audios:
@@ -42,9 +43,25 @@ def save_temp_audio(audio_file):
42
  return temp_audio
43
  return None
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file):
46
- # Debug Print, um zu sehen, ob Werte korrekt ankommen
47
- print(f"DEBUG: Font Size: {font_size}, Y-Pos: {y_pos}, Fade: {fade_duration}")
48
 
49
  if not images:
50
  return None, "❌ Keine Bilder ausgewählt"
@@ -54,58 +71,135 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
54
 
55
  # Text in Wörter aufteilen
56
  words = input_text.split() if input_text else []
57
- word_index = 0
 
 
 
 
 
 
58
 
59
- # Audio verarbeiten
60
  temp_audio_file = None
61
  if audio_file:
62
  temp_audio_file = save_temp_audio(audio_file)
63
 
64
- # Schriftart finden
65
- font_path = get_font_path()
66
- font_option = f":fontfile='{font_path}'" if font_path else ""
67
-
68
- for i, img_path in enumerate(images):
69
- img_path = Path(img_path.name)
70
- clip_path_with_text = Path(temp_dir) / f"clip_with_text_{i}.mp4"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- # Aktuelles Wort holen
73
- if word_index < len(words):
74
- text = words[word_index]
75
- word_index += 1
76
- else:
77
- text = ""
78
-
79
- # WICHTIG: Text in temporäre Datei schreiben, um Escaping-Probleme zu vermeiden
80
- text_file_path = Path(temp_dir) / f"text_{i}.txt"
81
- with open(text_file_path, "w", encoding="utf-8") as f:
82
- f.write(text)
83
-
84
- # Drawtext Filter mit textfile statt text='...'
85
- # box=1 macht einen leichten Hintergrund hinter den Text für Lesbarkeit
86
- vf_filters = (
87
  "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
88
  "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
89
- "fps=25,format=yuv420p,"
90
- f"drawtext=textfile='{text_file_path}'{font_option}:fontcolor=white:fontsize={font_size}:borderw=2:bordercolor=black:"
91
- f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:"
92
- f"alpha='if(lt(t,{fade_duration}), t/{fade_duration}, if(lt(t,{duration_per_image}-{fade_duration}), 1, ({duration_per_image}-t)/{fade_duration}))'"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  )
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  cmd = [
96
  "ffmpeg", "-y", "-loop", "1", "-i", str(img_path),
97
  "-t", str(duration_per_image),
98
- "-vf", vf_filters,
99
- str(clip_path_with_text)
100
  ]
101
 
102
  try:
103
  subprocess.run(cmd, check=True, capture_output=True, text=True)
104
- clips_with_text.append(clip_path_with_text)
105
  except subprocess.CalledProcessError as e:
106
- return None, f"❌ FFmpeg Fehler bei Bild {i+1}:\n{e.stderr}"
107
 
108
- # Zusammenfügen
109
  filelist_path = Path(temp_dir) / "filelist.txt"
110
  with open(filelist_path, "w") as f:
111
  for clip in clips_with_text:
@@ -120,7 +214,10 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
120
  str(output_video)
121
  ]
122
 
123
- subprocess.run(cmd_concat, check=True)
 
 
 
124
 
125
  # Audio hinzufügen falls vorhanden
126
  if temp_audio_file:
@@ -130,7 +227,10 @@ def generate_slideshow_with_audio(images, input_text, duration_per_word, duratio
130
  "-c:v", "copy", "-c:a", "aac", "-shortest",
131
  str(final_output)
132
  ]
133
- subprocess.run(cmd_audio, check=True)
 
 
 
134
  return str(final_output), "✅ Video mit Audio erstellt!"
135
 
136
  return str(output_video), "✅ Video erstellt (ohne Audio)"
@@ -141,16 +241,14 @@ with gr.Blocks() as demo:
141
 
142
  with gr.Row():
143
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
144
- text_input = gr.Textbox(label="Text", lines=5, placeholder="Wörter werden auf Bilder verteilt")
145
 
146
  with gr.Row():
147
- duration_image_input = gr.Number(value=3, label="Dauer pro Bild (s)")
 
148
  fade_input = gr.Number(value=0.5, label="Fade Dauer (s)")
149
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
150
- ypos_input = gr.Slider(0.0, 1.0, value=0.5, label="Y-Position (0=Oben, 1=Unten)")
151
-
152
- # Dummy Input für duration_per_word (wird im Script aktuell nicht genutzt, aber die Funk erwartet ihn)
153
- duration_word_input = gr.Number(value=0.5, visible=False)
154
 
155
  audio_input = gr.File(label="Audio (optional)", file_types=allowed_audios)
156
  btn = gr.Button("Erstellen", variant="primary")
@@ -165,11 +263,11 @@ with gr.Blocks() as demo:
165
  inputs=[
166
  img_input,
167
  text_input,
168
- duration_word_input,
169
  duration_image_input,
170
- fade_input, # War vorher vertauscht
171
- font_size_input, # War vorher vertauscht
172
- ypos_input, # War vorher vertauscht
173
  audio_input
174
  ],
175
  outputs=[out_video, status]
 
23
  return None # Fallback: FFmpeg soll selbst suchen (klappt manchmal nicht)
24
 
25
  def save_temp_audio(audio_file):
26
+ # Diese Funktion wurde nicht verändert, nur zur Vollständigkeit belassen
27
  if isinstance(audio_file, str):
28
  ext = Path(audio_file).suffix
29
  if ext.lower() not in allowed_audios:
 
43
  return temp_audio
44
  return None
45
 
46
+ # HILFSFUNKTION für den Drawtext-Filter
47
+ def create_timed_drawtext(word, start_time, duration, font_option, font_size, y_pos):
48
+ """Erstellt einen FFmpeg drawtext Filter, der ein Wort für eine bestimmte Zeit einblendet."""
49
+ # Definiere die Start- und Endzeit des Wortes
50
+ end_time = start_time + duration
51
+
52
+ # Text-Filter mit enable='between(t,start_time,end_time)' für zeitgesteuerte Anzeige
53
+ # box=1 wird weggelassen, um den Filter nicht zu komplex zu machen, falls jedes Wort einen eigenen Hintergrund braucht.
54
+ # Hier verwenden wir einen festen Rahmen (borderw) für Lesbarkeit.
55
+ drawtext_filter = (
56
+ f"drawtext=text='{word.replace(':', '\\:')}'{font_option}:fontcolor=white:fontsize={font_size}:borderw=2:bordercolor=black:"
57
+ f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:"
58
+ f"enable='between(t,{start_time},{end_time})'"
59
+ )
60
+ return drawtext_filter
61
+
62
  def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file):
63
+ # Debug Print
64
+ print(f"DEBUG: Font Size: {font_size}, Y-Pos: {y_pos}, Duration/Word: {duration_per_word}, Fade: {fade_duration}")
65
 
66
  if not images:
67
  return None, "❌ Keine Bilder ausgewählt"
 
71
 
72
  # Text in Wörter aufteilen
73
  words = input_text.split() if input_text else []
74
+
75
+ # Berechne die Gesamt-Textdauer
76
+ total_text_duration = len(words) * duration_per_word
77
+
78
+ # Schriftart finden
79
+ font_path = get_font_path()
80
+ font_option = f":fontfile='{font_path}'" if font_path else ""
81
 
82
+ # Audio verarbeiten (Muss vor dem ersten FFmpeg-Aufruf sein, um die Datei zu speichern)
83
  temp_audio_file = None
84
  if audio_file:
85
  temp_audio_file = save_temp_audio(audio_file)
86
 
87
+ # 1. ERSTES BILD: Hier wird die sequenzielle Textanzeige angewendet
88
+
89
+ # Stelle sicher, dass die Dauer des ersten Clips lang genug für den gesamten Text ist
90
+ # Wir nehmen die maximale Dauer zwischen der gewünschten Bilddauer und der Textdauer
91
+ duration_clip_1 = max(duration_per_image, total_text_duration)
92
+
93
+ # Generiere die sequentiellen Drawtext-Filter
94
+ drawtext_filters = []
95
+ current_time = 0.0
96
+ for word in words:
97
+ # Erstelle den Filter für das aktuelle Wort
98
+ # Wir verwenden duration_per_word als die Anzeigezeit für jedes Wort
99
+ filter_str = create_timed_drawtext(word, current_time, duration_per_word, font_option, font_size, y_pos)
100
+ drawtext_filters.append(filter_str)
101
+ # Nächste Startzeit erhöhen
102
+ current_time += duration_per_word
103
+
104
+ # FÜGE FADE HINZU: Wir wenden den Fade-Filter nur auf den Video-Stream an.
105
+ # Der Text-Stream wird nur über die 'enable' Bedingung gesteuert.
106
+ fade_filter = f"[v]fade=t=in:st=0:d={fade_duration},fade=t=out:st={duration_clip_1}-{fade_duration}:d={fade_duration}[v_out]"
107
+
108
+ # FÜGE ALLE FILTER ZUSAMMEN: scale, pad, fps, format, [Text-Filter], fade
109
+ # Jeder Text-Filter muss über das Overlay-Filter auf den Stream angewendet werden.
110
+ # Dies ist sehr komplex und einfacher, indem man alle drawtext-Filter in einem einzigen
111
+ # drawtext-Aufruf bündelt, ODER den Text nur für das erste Bild nutzt.
112
+
113
+ # Vereinfachte Methode: Nur der primäre Filterstring für das 1. Bild
114
+ if drawtext_filters:
115
+ # Fügen Sie alle drawtext-Filter (mit Komma getrennt) zum Haupt-VF-Filter hinzu
116
+ # Die Kette sieht dann so aus: scale -> pad -> fps -> format -> drawtext_1 -> drawtext_2 -> ... -> fade
117
+ # Da drawtext direkt auf den Stream angewendet wird, müssen wir mit dem overlay-Filter arbeiten,
118
+ # was bei mehreren Wörtern sehr komplex wird.
119
+ # Wir machen es uns einfacher und nutzen eine Kette von 'drawtext' Filtern.
120
+ # **ACHTUNG: Dies ist technisch nicht korrekt für *zeitgesteuertes* Einblenden, wenn man nur ein 'drawtext' nutzt.
121
+ # Stattdessen nutzen wir die 'enable' Bedingung in einem einzigen 'drawtext'-Aufruf,
122
+ # was einfacher ist, aber jedes Wort einzeln als drawtext-Aufruf benötigt.**
123
+
124
+ # Um es einfach zu halten, verwenden wir die `create_timed_drawtext` Funktion,
125
+ # die den `enable` Parameter nutzt. Wir fügen die einzelnen Filter-Anweisungen
126
+ # in einer Kette zusammen.
127
+
128
+ # **Korrektur:** Statt einer Kette muss man für jedes Wort einen eigenen `drawtext` Filter
129
+ # in der FFmpeg-Kommandozeile verwenden, verbunden durch Kommas.
130
 
131
+ # 1. Basisanpassungen
132
+ base_filters = (
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
134
  "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
135
+ "fps=25,format=yuv420p"
136
+ )
137
+
138
+ # 2. Sequentielle Textfilter
139
+ # Der erste Filter (base_filters) wird direkt angewendet, dann kommen die drawtext-Filter
140
+ # und ganz zum Schluss der Fade-Filter.
141
+
142
+ # Fügen Sie die Drawtext-Filter hinzu
143
+ all_drawtext_filters = ",".join(drawtext_filters)
144
+
145
+ # 3. Fade-Filter: Muss der letzte in der Kette sein, um das Ein- und Ausblenden des Bildes zu steuern.
146
+ # Hier ist es einfacher, das Bild (anstatt des gesamten Videos) einzublenden.
147
+ fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={duration_clip_1}-{fade_duration}:d={fade_duration}"
148
+
149
+ vf_filters_clip1 = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
150
+ else:
151
+ # Kein Text, nur Bild-Filter mit Fade
152
+ fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={duration_clip_1}-{fade_duration}:d={fade_duration}"
153
+ vf_filters_clip1 = (
154
+ "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
155
+ "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
156
+ f"fps=25,format=yuv420p,{fade_img_filter}"
157
  )
158
 
159
+ # Erstelle Clip 1 (mit Text)
160
+ img_path_1 = Path(images[0].name)
161
+ clip_path_1 = Path(temp_dir) / "clip_with_text_0.mp4"
162
+
163
+ cmd_1 = [
164
+ "ffmpeg", "-y", "-loop", "1", "-i", str(img_path_1),
165
+ "-t", str(duration_clip_1),
166
+ "-vf", vf_filters_clip1,
167
+ str(clip_path_1)
168
+ ]
169
+
170
+ try:
171
+ subprocess.run(cmd_1, check=True, capture_output=True, text=True)
172
+ clips_with_text.append(clip_path_1)
173
+ except subprocess.CalledProcessError as e:
174
+ return None, f"❌ FFmpeg Fehler bei Bild 1 (mit Text):\n{e.stderr}"
175
+
176
+ # 2. FOLGE-BILDER: Nur Bild mit Fade
177
+ for i in range(1, len(images)):
178
+ img_path = Path(images[i].name)
179
+ clip_path = Path(temp_dir) / f"clip_{i}.mp4"
180
+
181
+ # Nur Bild-Filter mit Fade
182
+ fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={duration_per_image}-{fade_duration}:d={fade_duration}"
183
+ vf_filters_clip = (
184
+ "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
185
+ "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
186
+ f"fps=25,format=yuv420p,{fade_img_filter}"
187
+ )
188
+
189
  cmd = [
190
  "ffmpeg", "-y", "-loop", "1", "-i", str(img_path),
191
  "-t", str(duration_per_image),
192
+ "-vf", vf_filters_clip,
193
+ str(clip_path)
194
  ]
195
 
196
  try:
197
  subprocess.run(cmd, check=True, capture_output=True, text=True)
198
+ clips_with_text.append(clip_path)
199
  except subprocess.CalledProcessError as e:
200
+ return None, f"❌ FFmpeg Fehler bei Bild {i+1} (ohne Text):\n{e.stderr}"
201
 
202
+ # Zusammenfügen (Der Rest der Funktion bleibt gleich)
203
  filelist_path = Path(temp_dir) / "filelist.txt"
204
  with open(filelist_path, "w") as f:
205
  for clip in clips_with_text:
 
214
  str(output_video)
215
  ]
216
 
217
+ try:
218
+ subprocess.run(cmd_concat, check=True)
219
+ except subprocess.CalledProcessError as e:
220
+ return None, f"❌ FFmpeg Fehler beim Zusammenfügen:\n{e.stderr}"
221
 
222
  # Audio hinzufügen falls vorhanden
223
  if temp_audio_file:
 
227
  "-c:v", "copy", "-c:a", "aac", "-shortest",
228
  str(final_output)
229
  ]
230
+ try:
231
+ subprocess.run(cmd_audio, check=True)
232
+ except subprocess.CalledProcessError as e:
233
+ return None, f"❌ FFmpeg Fehler beim Hinzufügen von Audio:\n{e.stderr}"
234
  return str(final_output), "✅ Video mit Audio erstellt!"
235
 
236
  return str(output_video), "✅ Video erstellt (ohne Audio)"
 
241
 
242
  with gr.Row():
243
  img_input = gr.Files(label="Bilder", file_types=allowed_medias)
244
+ text_input = gr.Textbox(label="Text (Wörter erscheinen nacheinander auf dem ersten Bild)", lines=5, placeholder="Jedes Wort wird für 'Dauer pro Wort' angezeigt.")
245
 
246
  with gr.Row():
247
+ duration_image_input = gr.Number(value=3, label="Dauer pro BILD (s) [für Bild 2+ und Min-Dauer für Bild 1]")
248
+ duration_word_input = gr.Number(value=1.0, label="Dauer pro WORT (s) [bestimmt Geschwindigkeit der Text-Anzeige]")
249
  fade_input = gr.Number(value=0.5, label="Fade Dauer (s)")
250
  font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
251
+ ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)")
 
 
 
252
 
253
  audio_input = gr.File(label="Audio (optional)", file_types=allowed_audios)
254
  btn = gr.Button("Erstellen", variant="primary")
 
263
  inputs=[
264
  img_input,
265
  text_input,
266
+ duration_word_input, # Jetzt aktiv
267
  duration_image_input,
268
+ fade_input,
269
+ font_size_input,
270
+ ypos_input,
271
  audio_input
272
  ],
273
  outputs=[out_video, status]