File size: 10,360 Bytes
e5b621e
085d5e6
1b78077
e5b621e
1b78077
fef9da1
c5cfcb5
e5b621e
377308b
 
8320d85
 
500f777
e5b621e
500f777
 
c5cfcb5
 
 
 
 
 
 
 
 
 
 
 
1bd9ab8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7644c1e
 
1bd9ab8
 
 
 
 
 
c5cfcb5
1ce3011
377308b
77f3dab
377308b
8320d85
77f3dab
8320d85
 
377308b
1ce3011
 
377308b
 
 
 
 
7644c1e
377308b
 
 
 
 
 
 
8320d85
1ce3011
8320d85
1ce3011
377308b
1ce3011
 
 
8320d85
c5cfcb5
7644c1e
ea1c088
 
c9a86ba
2a9840f
7644c1e
ad4cab5
c5cfcb5
7644c1e
 
1ce3011
7644c1e
 
 
1ce3011
7644c1e
 
 
1ce3011
 
 
fd9d93c
8320d85
1bd9ab8
7644c1e
d24cfba
8320d85
7644c1e
 
1ce3011
7644c1e
77f3dab
7644c1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ce3011
 
7644c1e
1ce3011
 
7644c1e
 
 
 
 
 
 
 
 
 
 
 
 
312fd62
c5cfcb5
7644c1e
1ce3011
 
312fd62
c5cfcb5
312fd62
 
1ce3011
312fd62
7644c1e
 
 
 
312fd62
7644c1e
c5cfcb5
 
312fd62
 
 
c5cfcb5
 
 
 
 
 
 
312fd62
c5cfcb5
1ce3011
7644c1e
1ce3011
7644c1e
 
1ce3011
bbb5565
7644c1e
 
ad4cab5
c5cfcb5
ad4cab5
c5cfcb5
 
ad4cab5
 
1ce3011
7644c1e
1ce3011
7644c1e
 
1ce3011
7644c1e
 
 
 
c5cfcb5
ad4cab5
7644c1e
 
36bfe7e
 
e5b621e
c5cfcb5
36bfe7e
c5cfcb5
 
7644c1e
 
b6a8e09
c5cfcb5
7644c1e
1ce3011
7644c1e
c5cfcb5
1ce3011
c5cfcb5
 
 
 
 
 
0b567d9
c5cfcb5
 
0b567d9
 
c5cfcb5
 
 
8320d85
c5cfcb5
1ce3011
 
 
c5cfcb5
 
0b567d9
 
 
c5cfcb5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import gradio as gr
import tempfile
from pathlib import Path
import uuid
import subprocess
import shutil
import os

# NEU: Dauer des Fade-In/Out für jedes einzelne Wort (z.B. 0.2 Sekunden)
WORD_FADE_DURATION = 0.2
FFMPEG_ESCAPE_CHAR = "\\"

# Erlaubte Dateiformate
allowed_medias = [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff"]
allowed_audios = [".mp3", ".wav", ".m4a", ".ogg"]

def get_font_path():
    """Versucht, eine Standard-Schriftart im Linux-System zu finden."""
    possible_fonts = [
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
        "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf"
    ]
    for font in possible_fonts:
        if os.path.exists(font):
            return font
    return None # Fallback: FFmpeg soll selbst suchen (klappt manchmal nicht)

def save_temp_audio(audio_file_path):
    """
    Speichert die hochgeladene Audio-Datei in einem temporären Verzeichnis.
    Erwartet einen Dateipfad-String von Gradio.
    """
    if not audio_file_path:
        return None, None

    # Gradio liefert einen String-Pfad zum temporären Speicherort
    input_path = Path(audio_file_path)
    
    # Bestimme die Erweiterung
    ext = input_path.suffix
    if ext.lower() not in allowed_audios:
        ext = ".mp3"
        
    # Erstelle das Zielverzeichnis und den Zielpfad
    temp_audio_dir = Path(tempfile.mkdtemp())
    temp_audio = temp_audio_dir / f"input{ext}"
    
    # Kopiere die Datei vom Gradio-Temp-Pfad in unseren eigenen Temp-Pfad
    try:
        shutil.copyfile(input_path, temp_audio)
        # Rückgabe des Verzeichnisses, das später gelöscht werden kann, und des Dateipfads
        return temp_audio_dir, temp_audio
    except Exception as e:
        print(f"Fehler beim Kopieren der Audiodatei: {e}")
        if temp_audio_dir.exists():
             shutil.rmtree(temp_audio_dir)
        return None, None


def create_timed_drawtext(word, start_time, duration, font_option, font_size, y_pos):
    """Erstellt einen FFmpeg drawtext Filter, der ein Wort mit weichen Übergängen (Alpha-Kanal) einblendet."""
    global FFMPEG_ESCAPE_CHAR 
    global WORD_FADE_DURATION 

    # 1. Escaping: Ersetze alle ":" durch "\:" für FFmpeg
    escaped_word = word.replace(':', f"{FFMPEG_ESCAPE_CHAR}:")
    
    # Definiere die Start- und Endzeit des WORTES
    end_time = start_time + duration
    
    # Zeitpunkte für den Fade
    fade_in_end = start_time + WORD_FADE_DURATION
    fade_out_start = end_time - WORD_FADE_DURATION
    
    # Alpha-Ausdruck für smooth Fade-In und Fade-Out
    # Steuert die Deckkraft basierend auf der Zeit t (relativ zum Clip-Start)
    alpha_expression = (
        f"if(lt(t,{start_time}), 0, "
        f"if(lt(t,{fade_in_end}), (t-{start_time})/{WORD_FADE_DURATION}, "
        f"if(lt(t,{fade_out_start}), 1, "
        f"if(lt(t,{end_time}), ({end_time}-t)/{WORD_FADE_DURATION}, 0))))"
    )

    # Erstelle den Filterstring
    drawtext_filter = (
        f"drawtext=text='{escaped_word}'{font_option}:fontcolor=white:fontsize={font_size}:borderw=2:bordercolor=black:"
        f"x=(w-text_w)/2:y=(h-text_h)*{y_pos}:"
        f"alpha='{alpha_expression}'" # Steuert die Deckkraft (Smoothness)
    )
    return drawtext_filter


def generate_slideshow_with_audio(images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file):
    
    if not images:
        return None, "❌ Keine Bilder ausgewählt"
    
    temp_dir = tempfile.mkdtemp()
    
    # Text in Wörter aufteilen
    words = input_text.split() if input_text else []
    total_words = len(words)
    num_images = len(images)
    
    # Berechnung der gleichmäßigen Verteilung der Wörter auf die Bilder
    base_words_per_clip = total_words // num_images
    remainder = total_words % num_images
    
    current_word_index = 0
    clips_with_text = [] # Paths der generierten MP4-Clips

    # Schriftart finden
    font_path = get_font_path()
    font_option = f":fontfile='{font_path}'" if font_path else ""

    # Audio verarbeiten
    # audio_file ist der Pfad-String von Gradio
    audio_temp_dir, temp_audio_file = save_temp_audio(audio_file) if audio_file else (None, None)

    
    # --- 1. SCHLEIFE: Erstelle jeden Clip mit seinem Textsegment ---
    for i in range(num_images):
        img_path = Path(images[i].name)
        clip_path = Path(temp_dir) / f"clip_with_text_{i}.mp4"

        # 1. Bestimme das Wortsegment für diesen Clip
        words_on_this_clip = base_words_per_clip + (1 if i < remainder else 0)
        
        # Extrahieren des Segments aus der Gesamtliste der Wörter
        word_segment = words[current_word_index : current_word_index + words_on_this_clip]
        current_word_index += len(word_segment)
        
        # 2. Berechne die Clip-Dauer
        text_duration = len(word_segment) * duration_per_word
        # Die Dauer ist das Maximum aus der gewünschten Bilddauer und der benötigten Textdauer
        duration_clip = max(duration_per_image, text_duration)

        # 3. Generiere Drawtext Filter (Startzeit ist relativ zum Clip-Start, also 0)
        drawtext_filters = []
        word_start_time = 0.0
        for word in word_segment:
            filter_str = create_timed_drawtext(word, word_start_time, duration_per_word, font_option, font_size, y_pos)
            drawtext_filters.append(filter_str)
            word_start_time += duration_per_word

        # 4. Basis- und Fade-Filter
        base_filters = (
            "scale=w=1280:h=720:force_original_aspect_ratio=decrease,"
            "pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black,"
            "fps=25,format=yuv420p"
        )
        
        fade_out_start = duration_clip - fade_duration
        if fade_out_start < 0: fade_out_start = 0
        fade_img_filter = f"fade=t=in:st=0:d={fade_duration},fade=t=out:st={fade_out_start}:d={fade_duration}"
        
        # 5. Kombiniere alle Filter
        if drawtext_filters:
            all_drawtext_filters = ",".join(drawtext_filters)
            vf_filters_clip = f"{base_filters},{all_drawtext_filters},{fade_img_filter}"
        else:
            # Kein Text mehr: Nur Bild mit Fade
            vf_filters_clip = f"{base_filters},{fade_img_filter}"

        # 6. FFmpeg Command zum Erstellen des Clips
        cmd = [
            "ffmpeg", "-y", "-loop", "1", "-i", str(img_path),
            "-t", str(duration_clip),
            "-vf", vf_filters_clip,
            str(clip_path)
        ]
        
        try:
            subprocess.run(cmd, check=True, capture_output=True, text=True)
            clips_with_text.append(clip_path)
        except subprocess.CalledProcessError as e:
            # Bereinigung bei Fehler
            shutil.rmtree(temp_dir)
            if audio_temp_dir: shutil.rmtree(audio_temp_dir)
            return None, f"❌ FFmpeg Fehler bei Bild {i+1}:\n{e.stderr}"

    # --- 2. ZUSAMMENFÜGEN ---
    filelist_path = Path(temp_dir) / "filelist.txt"
    with open(filelist_path, "w") as f:
        for clip in clips_with_text:
            f.write(f"file '{clip}'\n")

    output_video = Path(temp_dir) / f"slideshow_{uuid.uuid4().hex}.mp4"
    
    cmd_concat = [
        "ffmpeg", "-y", "-f", "concat", "-safe", "0",
        "-i", str(filelist_path),
        "-c:v", "libx264", "-pix_fmt", "yuv420p",
        str(output_video)
    ]
    
    try:
        subprocess.run(cmd_concat, check=True, capture_output=True, text=True)
    except subprocess.CalledProcessError as e:
        shutil.rmtree(temp_dir)
        if audio_temp_dir: shutil.rmtree(audio_temp_dir)
        return None, f"❌ FFmpeg Fehler beim Zusammenfügen:\n{e.stderr}"

    # --- 3. AUDIO HINZUFÜGEN (falls vorhanden) ---
    final_output = output_video
    if temp_audio_file:
        final_output = Path(temp_dir) / f"final_{uuid.uuid4().hex}.mp4"
        cmd_audio = [
            "ffmpeg", "-y", "-i", str(output_video), "-i", str(temp_audio_file),
            "-c:v", "copy", "-c:a", "aac", "-shortest",
            str(final_output)
        ]
        try:
            subprocess.run(cmd_audio, check=True, capture_output=True, text=True)
        except subprocess.CalledProcessError as e:
            shutil.rmtree(temp_dir)
            if audio_temp_dir: shutil.rmtree(audio_temp_dir)
            return None, f"❌ FFmpeg Fehler beim Hinzufügen von Audio:\n{e.stderr}"
        
        # Bereinige das separate Audio-Temp-Verzeichnis
        if audio_temp_dir: shutil.rmtree(audio_temp_dir)
        
        return str(final_output), "✅ Video mit Audio erstellt!"

    # Nur Video-Pfad zurückgeben
    return str(final_output), "✅ Video erstellt (ohne Audio)"

# Gradio UI
with gr.Blocks() as demo:
    gr.Markdown("# Slideshow Generator")

    with gr.Row():
        img_input = gr.Files(label="Bilder", file_types=allowed_medias)
        # TEXT WURDE GEÄNDERT: Neue Beschreibung für Textverteilung
        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.")
    
    with gr.Row():
        duration_image_input = gr.Number(value=3, label="Mindest-Dauer pro BILD (s)")
        duration_word_input = gr.Number(value=1.0, label="Dauer pro WORT (s) [bestimmt Geschwindigkeit der Text-Anzeige]")
        fade_input = gr.Number(value=0.5, label="Bild-Fade Dauer (s)")
        font_size_input = gr.Number(value=80, label="Schriftgröße (px)")
        ypos_input = gr.Slider(0.0, 1.0, value=0.9, label="Y-Position (0=Oben, 1=Unten)")
    
    audio_input = gr.File(label="Audio (optional)", file_types=allowed_audios)
    btn = gr.Button("Erstellen", variant="primary")
    
    out_video = gr.Video(label="Ergebnis")
    status = gr.Textbox(label="Status")

    # KORREKTE REIHENFOLGE DER INPUTS:
    # (images, input_text, duration_per_word, duration_per_image, fade_duration, font_size, y_pos, audio_file)
    btn.click(
        fn=generate_slideshow_with_audio,
        inputs=[
            img_input, 
            text_input, 
            duration_word_input, 
            duration_image_input, 
            fade_input, 
            font_size_input, 
            ypos_input, 
            audio_input
        ],
        outputs=[out_video, status]
    )

demo.launch()