From 5edc3b9f3ace1da34963fd57014d7cf83f22d2ea Mon Sep 17 00:00:00 2001 From: Fedja Windows Date: Sat, 26 Apr 2025 10:04:08 +0200 Subject: [PATCH] api funktioniert --- api_server.py | 252 ++++++++++++++++++++++++++++++++++--------- hunyuan_cli.py | 54 +++++++++- start_api_server.bat | 2 +- 3 files changed, 252 insertions(+), 56 deletions(-) diff --git a/api_server.py b/api_server.py index b0b0e7e..ffa8671 100644 --- a/api_server.py +++ b/api_server.py @@ -1,15 +1,26 @@ -# api_server.py - REST-API für den Videogenerator +# api_server_fixed.py - Korrigierte REST-API für den Videogenerator mit job_id-Unterstützung # Im webui-Verzeichnis speichern -from fastapi import FastAPI, File, UploadFile, BackgroundTasks, Form +from fastapi import FastAPI, BackgroundTasks from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware import uvicorn import os import subprocess import uuid -import shutil -from typing import Optional +from pydantic import BaseModel +from typing import Optional, List, Dict + +# Datenmodell für die Videogenerierungsanfrage +class VideoGenerationRequest(BaseModel): + image_path: str + prompt: str + n_prompt: Optional[str] = "" + seed: Optional[int] = 31337 + length: Optional[float] = 5.0 + steps: Optional[int] = 25 + use_teacache: Optional[bool] = True + output_filename: Optional[str] = "output.mp4" app = FastAPI(title="Hunyuan Video Generator API") @@ -22,30 +33,45 @@ app.add_middleware( allow_headers=["*"], ) -# Verzeichnisse für temporäre Dateien und Ausgaben -TEMP_DIR = os.path.join(os.path.dirname(__file__), "temp_uploads") +# Verzeichnis für Ausgaben OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "outputs") PYTHON_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "system", "python", "python.exe") -os.makedirs(TEMP_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True) +# In-Memory Speicher für laufende Jobs +active_jobs = {} + def generate_video_task(image_path: str, prompt: str, n_prompt: str, seed: int, length: float, steps: int, use_teacache: bool, output_filename: str, job_id: str): """Führt die Videogenerierung als Hintergrundaufgabe aus""" - # Befehlszeile erstellen + # Absoluten Pfad sicherstellen + if not os.path.isabs(image_path): + # Wenn relativer Pfad, relativ zum aktuellen Verzeichnis interpretieren + image_path = os.path.abspath(os.path.join(os.path.dirname(__file__), image_path)) + + # Prüfen, ob die Bilddatei existiert + if not os.path.exists(image_path): + print(f"Fehler: Bilddatei nicht gefunden: {image_path}") + error_file = os.path.join(OUTPUT_DIR, f"{job_id}_error.txt") + with open(error_file, 'w') as f: + f.write(f"Fehler: Bilddatei nicht gefunden: {image_path}") + return + + # Befehlszeile erstellen - WICHTIG: Wir geben die job_id als Parameter weiter cmd = [ PYTHON_PATH, - "hunyuan_cli.py", + "hunyuan_cli.py", # Aktualisierte Version mit job_id-Unterstützung "--image", image_path, "--prompt", prompt, "--seed", str(seed), "--length", str(length), "--steps", str(steps), - "--output", f"{job_id}_{output_filename}" + "--output", output_filename, + "--job_id", job_id # Wichtig: Gebe job_id weiter ] if n_prompt: @@ -54,49 +80,70 @@ def generate_video_task(image_path: str, prompt: str, n_prompt: str, if use_teacache: cmd.append("--teacache") - # Befehl ausführen + # Befehl ausführen und Ausgabe protokollieren try: - subprocess.run(cmd, check=True, cwd=os.path.dirname(__file__)) + print(f"Starte Videogenerierung mit Befehl: {' '.join(cmd)}") + process = subprocess.run(cmd, + check=True, + cwd=os.path.dirname(__file__), + capture_output=True, + text=True) + + # Ausgabe in Protokolldatei schreiben + log_path = os.path.join(OUTPUT_DIR, f"{job_id}_log.txt") + with open(log_path, 'w') as log_file: + log_file.write(f"STDOUT:\n{process.stdout}\n\nSTDERR:\n{process.stderr}") + print(f"Video generation completed for job {job_id}") + + # Aus aktiven Jobs entfernen + if job_id in active_jobs: + del active_jobs[job_id] + except subprocess.CalledProcessError as e: print(f"Error generating video: {e}") + # Fehlermeldung in Protokolldatei schreiben + error_path = os.path.join(OUTPUT_DIR, f"{job_id}_error.txt") + with open(error_path, 'w') as error_file: + error_file.write(f"Command: {' '.join(cmd)}\n\nError:\n{str(e)}\n\nSTDOUT:\n{e.stdout}\n\nSTDERR:\n{e.stderr}") + + # Aus aktiven Jobs entfernen + if job_id in active_jobs: + del active_jobs[job_id] @app.post("/generate/") async def generate_video( - background_tasks: BackgroundTasks, - image: UploadFile = File(...), - prompt: str = Form(...), - n_prompt: Optional[str] = Form(""), - seed: Optional[int] = Form(31337), - length: Optional[float] = Form(5.0), - steps: Optional[int] = Form(25), - use_teacache: Optional[bool] = Form(True), - output_filename: Optional[str] = Form("output.mp4") + request: VideoGenerationRequest, + background_tasks: BackgroundTasks ): """ - Generiert ein Video basierend auf den angegebenen Parametern + Generiert ein Video basierend auf den angegebenen Parametern und einem lokalen Bildpfad """ # Eindeutige Job-ID generieren job_id = str(uuid.uuid4()) - # Temporären Dateipfad für das Bild erstellen - temp_image_path = os.path.join(TEMP_DIR, f"{job_id}_{image.filename}") + # In aktive Jobs aufnehmen + active_jobs[job_id] = { + "status": "initializing", + "parameters": request.dict() + } - # Bild speichern - with open(temp_image_path, "wb") as buffer: - shutil.copyfileobj(image.file, buffer) + # Statusdatei sofort erstellen + status_path = os.path.join(OUTPUT_DIR, f"{job_id}_status.txt") + with open(status_path, 'w') as f: + f.write(f"Status: Initialisierung\nJob-ID: {job_id}\nBild: {request.image_path}\nPrompt: {request.prompt}\n") # Videogenerierung als Hintergrundaufgabe starten background_tasks.add_task( generate_video_task, - temp_image_path, - prompt, - n_prompt, - seed, - length, - steps, - use_teacache, - output_filename, + request.image_path, + request.prompt, + request.n_prompt, + request.seed, + request.length, + request.steps, + request.use_teacache, + request.output_filename, job_id ) @@ -104,7 +151,7 @@ async def generate_video( "status": "processing", "job_id": job_id, "message": "Video generation started in background", - "result_url": f"/result/{job_id}_{output_filename}" + "result_url": f"/result/{job_id}_{request.output_filename}" } @app.get("/result/{filename}") @@ -118,33 +165,138 @@ async def get_result(filename: str): return FileResponse( file_path, media_type="video/mp4", - filename=filename.split("_", 1)[1] # Entferne Job-ID vom Dateinamen + filename=filename.split("_", 1)[1] if "_" in filename else filename # Entferne Job-ID vom Dateinamen ) else: - return {"status": "not_found", "message": "Requested video not found or still processing"} + return {"status": "not_found", "message": f"Requested video not found or still processing: {file_path}"} @app.get("/status/{job_id}") async def check_status(job_id: str): """ Prüft den Status einer Videogenerierung """ - # Suche nach Dateien, die mit der Job-ID beginnen - result_files = [f for f in os.listdir(OUTPUT_DIR) if f.startswith(job_id)] + # 1. Prüfen, ob der Job in den aktiven Jobs ist + if job_id in active_jobs: + return { + "status": "processing", + "job_id": job_id, + "details": "Job wird verarbeitet", + "active_job": True + } - if result_files: + # 2. Prüfen, ob eine Statusdatei existiert + status_file = os.path.join(OUTPUT_DIR, f"{job_id}_status.txt") + if os.path.exists(status_file): + with open(status_file, 'r') as f: + status_content = f.read() + + # 3. Prüfen, ob ein fertiges Video existiert + video_files = [f for f in os.listdir(OUTPUT_DIR) + if f.startswith(job_id) and f.endswith('.mp4')] + + if video_files: + return { + "status": "completed", + "job_id": job_id, + "files": video_files, + "download_urls": [f"/result/{file}" for file in video_files], + "status_info": status_content + } + + # 4. Prüfen, ob ein Fehler aufgetreten ist + error_file = os.path.join(OUTPUT_DIR, f"{job_id}_error.txt") + if os.path.exists(error_file): + with open(error_file, 'r') as f: + error_text = f.read() + return { + "status": "error", + "job_id": job_id, + "error": error_text, + "status_info": status_content + } + + # 5. Sonst läuft der Job noch + return { + "status": "processing", + "job_id": job_id, + "status_info": status_content + } + + # 6. Prüfen auf alte Dateien ohne Status-Datei + video_files = [f for f in os.listdir(OUTPUT_DIR) + if f.startswith(job_id) and f.endswith('.mp4')] + + if video_files: return { "status": "completed", "job_id": job_id, - "files": result_files, - "download_urls": [f"/result/{file}" for file in result_files] + "files": video_files, + "download_urls": [f"/result/{file}" for file in video_files], + "note": "Status file not found, but video exists" } - else: - # Prüfen, ob das Eingabebild noch existiert (Verarbeitung läuft noch) - input_files = [f for f in os.listdir(TEMP_DIR) if f.startswith(job_id)] - if input_files: - return {"status": "processing", "job_id": job_id} - else: - return {"status": "not_found", "job_id": job_id} + + # 7. Prüfen, ob ein Fehlerprotokoll existiert ohne Status-Datei + error_file = os.path.join(OUTPUT_DIR, f"{job_id}_error.txt") + if os.path.exists(error_file): + with open(error_file, 'r') as f: + error_text = f.read() + return { + "status": "error", + "job_id": job_id, + "error": error_text, + "note": "Status file not found, but error exists" + } + + # 8. Prüfen, ob ein Ausführungsprotokoll existiert ohne Status-Datei + log_file = os.path.join(OUTPUT_DIR, f"{job_id}_log.txt") + if os.path.exists(log_file): + return { + "status": "unknown", + "job_id": job_id, + "note": "Status file not found, but log exists, check logs for details" + } + + # 9. Keine Dateien oder Informationen zu dieser Job-ID gefunden + return {"status": "not_found", "job_id": job_id} + +@app.get("/debug/") +async def debug(): + """ + Gibt Debug-Informationen zurück + """ + try: + # Alle Dateien im OUTPUT_DIR auflisten + files = os.listdir(OUTPUT_DIR) + + # Gruppieren nach Präfix + file_groups = {} + for file in files: + parts = file.split('_', 1) + if len(parts) > 1: + prefix = parts[0] + if prefix not in file_groups: + file_groups[prefix] = [] + file_groups[prefix].append(file) + + return { + "active_jobs": active_jobs, + "output_dir": OUTPUT_DIR, + "files_count": len(files), + "file_groups": file_groups + } + except Exception as e: + return {"error": str(e)} + +@app.get("/debug/files/") +async def list_files(): + """ + Listet alle Dateien im OUTPUT_DIR auf + """ + try: + files = os.listdir(OUTPUT_DIR) + return {"files": files} + except Exception as e: + return {"error": str(e)} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/hunyuan_cli.py b/hunyuan_cli.py index bb81706..edba6f7 100644 --- a/hunyuan_cli.py +++ b/hunyuan_cli.py @@ -1,4 +1,4 @@ -# hunyuan_cli.py - Kommandozeilen-Interface für den Videogenerator +# hunyuan_cli_fixed.py - Kommandozeilen-Interface für den Videogenerator mit job_id-Unterstützung from diffusers_helper.hf_login import login import os import argparse @@ -36,6 +36,7 @@ parser.add_argument('--gpu_mem', type=float, default=6.0, help='GPU Speicherrese parser.add_argument('--teacache', action='store_true', help='TeaCache aktivieren') parser.add_argument('--mp4_crf', type=int, default=16, help='MP4 Kompression (0-100)') parser.add_argument('--output', type=str, default='output.mp4', help='Ausgabedatei (MP4)') +parser.add_argument('--job_id', type=str, default='', help='Job-ID (wenn von API aufgerufen)') args = parser.parse_args() @@ -120,16 +121,29 @@ def process_cli(): use_teacache = args.teacache mp4_crf = args.mp4_crf output_path = args.output + + # WICHTIG: Job-ID entweder von Parameter nehmen oder einen Zeitstempel generieren + job_id = args.job_id if args.job_id else generate_timestamp() + + # Status-Datei erstellen oder aktualisieren + status_file = os.path.join(outputs_folder, f"{job_id}_status.txt") + with open(status_file, "w") as f: + f.write(f"Status: Initialisierung\nJob-ID: {job_id}\nStart-Zeit: {generate_timestamp()}\n") total_latent_sections = (total_second_length * 30) / (latent_window_size * 4) total_latent_sections = int(max(round(total_latent_sections), 1)) - job_id = generate_timestamp() final_output_path = os.path.join(outputs_folder, output_path) + final_output_path_with_job_id = os.path.join(outputs_folder, f"{job_id}_{output_path}") try: + print(f"Job-ID: {job_id}") + # Eingabebild laden print("Lade Eingabebild...") + with open(status_file, "a") as f: + f.write(f"Status: Lade Eingabebild\n") + input_image = np.array(Image.open(input_image_path)) # Clean GPU @@ -140,6 +154,9 @@ def process_cli(): # Text encoding print("Text-Encoding...") + with open(status_file, "a") as f: + f.write(f"Status: Text-Encoding\n") + if not high_vram: fake_diffusers_current_device(text_encoder, gpu) load_model_as_complete(text_encoder_2, target_device=gpu) @@ -156,17 +173,24 @@ def process_cli(): # Bild verarbeiten print("Verarbeite Eingabebild...") + with open(status_file, "a") as f: + f.write(f"Status: Verarbeite Eingabebild\n") + H, W, C = input_image.shape height, width = find_nearest_bucket(H, W, resolution=640) input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height) - Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}.png')) + # WICHTIG: Speichere das Input-Bild mit job_id Präfix + Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}_input.png')) input_image_pt = torch.from_numpy(input_image_np).float() / 127.5 - 1 input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None] # VAE encoding print("VAE-Encoding...") + with open(status_file, "a") as f: + f.write(f"Status: VAE-Encoding\n") + if not high_vram: load_model_as_complete(vae, target_device=gpu) @@ -174,6 +198,9 @@ def process_cli(): # CLIP Vision print("CLIP Vision-Encoding...") + with open(status_file, "a") as f: + f.write(f"Status: CLIP Vision-Encoding\n") + if not high_vram: load_model_as_complete(image_encoder, target_device=gpu) @@ -189,6 +216,9 @@ def process_cli(): # Sampling print("Starte Sampling...") + with open(status_file, "a") as f: + f.write(f"Status: Starte Sampling\n") + rnd = torch.Generator("cpu").manual_seed(seed) num_frames = latent_window_size * 4 - 3 @@ -206,6 +236,8 @@ def process_cli(): latent_padding_size = latent_padding * latent_window_size print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}') + with open(status_file, "a") as f: + f.write(f"Sampling: latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}\n") indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0) clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1) @@ -228,6 +260,8 @@ def process_cli(): current_step = d['i'] + 1 percentage = int(100.0 * current_step / steps) print(f'Sampling {current_step}/{steps} ({percentage}%)') + with open(status_file, "a") as f: + f.write(f"Sampling Schritt {current_step}/{steps} ({percentage}%)\n") generated_latents = sample_hunyuan( transformer=transformer, @@ -283,22 +317,32 @@ def process_cli(): if not high_vram: unload_complete_models() + # WICHTIG: Der temporäre Ausgabefilename enthält die job_id tmp_output_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}.mp4') save_bcthw_as_mp4(history_pixels, tmp_output_filename, fps=30, crf=mp4_crf) print(f'Decoded. Aktuelle Latent Shape {real_history_latents.shape}; Pixel Shape {history_pixels.shape}') + with open(status_file, "a") as f: + f.write(f'Dekodiert. Aktuelle Latent Shape {real_history_latents.shape}; Pixel Shape {history_pixels.shape}\n') if is_last_section: # Kopiere das finale Video zum gewünschten Ausgabepfad import shutil - shutil.copy(tmp_output_filename, final_output_path) + # Stelle sicher, dass die finale Ausgabedatei auch die job_id enthält + shutil.copy(tmp_output_filename, final_output_path_with_job_id) break - print(f"Video erfolgreich erstellt: {final_output_path}") + with open(status_file, "a") as f: + f.write(f"Status: Abgeschlossen\nErgebnis: {final_output_path_with_job_id}\n") + + print(f"Video erfolgreich erstellt: {final_output_path_with_job_id}") except Exception as e: traceback.print_exc() + with open(status_file, "a") as f: + f.write(f"Status: Fehler\nFehlermeldung: {str(e)}\n{traceback.format_exc()}\n") + print(f"Fehler bei der Videogenerierung: {str(e)}") if not high_vram: diff --git a/start_api_server.bat b/start_api_server.bat index 9672122..c404882 100644 --- a/start_api_server.bat +++ b/start_api_server.bat @@ -6,7 +6,7 @@ cd %~dp0webui "%DIR%\python\python.exe" -m pip install fastapi uvicorn python-multipart -"%DIR%\python\python.exe" api_server.py +"%DIR%\python\python.exe" api_server_path.py :done pause \ No newline at end of file