api funktioniert

This commit is contained in:
Fedja Windows 2025-04-26 10:04:08 +02:00
parent 88fc7017e6
commit 5edc3b9f3a
3 changed files with 252 additions and 56 deletions

View File

@ -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 # Im webui-Verzeichnis speichern
from fastapi import FastAPI, File, UploadFile, BackgroundTasks, Form from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import uvicorn import uvicorn
import os import os
import subprocess import subprocess
import uuid import uuid
import shutil from pydantic import BaseModel
from typing import Optional 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") app = FastAPI(title="Hunyuan Video Generator API")
@ -22,30 +33,45 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Verzeichnisse für temporäre Dateien und Ausgaben # Verzeichnis für Ausgaben
TEMP_DIR = os.path.join(os.path.dirname(__file__), "temp_uploads")
OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "outputs") 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") 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) 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, def generate_video_task(image_path: str, prompt: str, n_prompt: str,
seed: int, length: float, steps: int, seed: int, length: float, steps: int,
use_teacache: bool, output_filename: str, use_teacache: bool, output_filename: str,
job_id: str): job_id: str):
"""Führt die Videogenerierung als Hintergrundaufgabe aus""" """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 = [ cmd = [
PYTHON_PATH, PYTHON_PATH,
"hunyuan_cli.py", "hunyuan_cli.py", # Aktualisierte Version mit job_id-Unterstützung
"--image", image_path, "--image", image_path,
"--prompt", prompt, "--prompt", prompt,
"--seed", str(seed), "--seed", str(seed),
"--length", str(length), "--length", str(length),
"--steps", str(steps), "--steps", str(steps),
"--output", f"{job_id}_{output_filename}" "--output", output_filename,
"--job_id", job_id # Wichtig: Gebe job_id weiter
] ]
if n_prompt: if n_prompt:
@ -54,49 +80,70 @@ def generate_video_task(image_path: str, prompt: str, n_prompt: str,
if use_teacache: if use_teacache:
cmd.append("--teacache") cmd.append("--teacache")
# Befehl ausführen # Befehl ausführen und Ausgabe protokollieren
try: 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}") 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: except subprocess.CalledProcessError as e:
print(f"Error generating video: {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/") @app.post("/generate/")
async def generate_video( async def generate_video(
background_tasks: BackgroundTasks, request: VideoGenerationRequest,
image: UploadFile = File(...), background_tasks: BackgroundTasks
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")
): ):
""" """
Generiert ein Video basierend auf den angegebenen Parametern Generiert ein Video basierend auf den angegebenen Parametern und einem lokalen Bildpfad
""" """
# Eindeutige Job-ID generieren # Eindeutige Job-ID generieren
job_id = str(uuid.uuid4()) job_id = str(uuid.uuid4())
# Temporären Dateipfad für das Bild erstellen # In aktive Jobs aufnehmen
temp_image_path = os.path.join(TEMP_DIR, f"{job_id}_{image.filename}") active_jobs[job_id] = {
"status": "initializing",
"parameters": request.dict()
}
# Bild speichern # Statusdatei sofort erstellen
with open(temp_image_path, "wb") as buffer: status_path = os.path.join(OUTPUT_DIR, f"{job_id}_status.txt")
shutil.copyfileobj(image.file, buffer) 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 # Videogenerierung als Hintergrundaufgabe starten
background_tasks.add_task( background_tasks.add_task(
generate_video_task, generate_video_task,
temp_image_path, request.image_path,
prompt, request.prompt,
n_prompt, request.n_prompt,
seed, request.seed,
length, request.length,
steps, request.steps,
use_teacache, request.use_teacache,
output_filename, request.output_filename,
job_id job_id
) )
@ -104,7 +151,7 @@ async def generate_video(
"status": "processing", "status": "processing",
"job_id": job_id, "job_id": job_id,
"message": "Video generation started in background", "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}") @app.get("/result/{filename}")
@ -118,33 +165,138 @@ async def get_result(filename: str):
return FileResponse( return FileResponse(
file_path, file_path,
media_type="video/mp4", 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: 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}") @app.get("/status/{job_id}")
async def check_status(job_id: str): async def check_status(job_id: str):
""" """
Prüft den Status einer Videogenerierung Prüft den Status einer Videogenerierung
""" """
# Suche nach Dateien, die mit der Job-ID beginnen # 1. Prüfen, ob der Job in den aktiven Jobs ist
result_files = [f for f in os.listdir(OUTPUT_DIR) if f.startswith(job_id)] 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 { return {
"status": "completed", "status": "completed",
"job_id": job_id, "job_id": job_id,
"files": result_files, "files": video_files,
"download_urls": [f"/result/{file}" for file in result_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) # 7. Prüfen, ob ein Fehlerprotokoll existiert ohne Status-Datei
input_files = [f for f in os.listdir(TEMP_DIR) if f.startswith(job_id)] error_file = os.path.join(OUTPUT_DIR, f"{job_id}_error.txt")
if input_files: if os.path.exists(error_file):
return {"status": "processing", "job_id": job_id} with open(error_file, 'r') as f:
else: error_text = f.read()
return {"status": "not_found", "job_id": job_id} 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__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@ -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 from diffusers_helper.hf_login import login
import os import os
import argparse 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('--teacache', action='store_true', help='TeaCache aktivieren')
parser.add_argument('--mp4_crf', type=int, default=16, help='MP4 Kompression (0-100)') 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('--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() args = parser.parse_args()
@ -120,16 +121,29 @@ def process_cli():
use_teacache = args.teacache use_teacache = args.teacache
mp4_crf = args.mp4_crf mp4_crf = args.mp4_crf
output_path = args.output 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 = (total_second_length * 30) / (latent_window_size * 4)
total_latent_sections = int(max(round(total_latent_sections), 1)) 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 = os.path.join(outputs_folder, output_path)
final_output_path_with_job_id = os.path.join(outputs_folder, f"{job_id}_{output_path}")
try: try:
print(f"Job-ID: {job_id}")
# Eingabebild laden # Eingabebild laden
print("Lade Eingabebild...") 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)) input_image = np.array(Image.open(input_image_path))
# Clean GPU # Clean GPU
@ -140,6 +154,9 @@ def process_cli():
# Text encoding # Text encoding
print("Text-Encoding...") print("Text-Encoding...")
with open(status_file, "a") as f:
f.write(f"Status: Text-Encoding\n")
if not high_vram: if not high_vram:
fake_diffusers_current_device(text_encoder, gpu) fake_diffusers_current_device(text_encoder, gpu)
load_model_as_complete(text_encoder_2, target_device=gpu) load_model_as_complete(text_encoder_2, target_device=gpu)
@ -156,17 +173,24 @@ def process_cli():
# Bild verarbeiten # Bild verarbeiten
print("Verarbeite Eingabebild...") print("Verarbeite Eingabebild...")
with open(status_file, "a") as f:
f.write(f"Status: Verarbeite Eingabebild\n")
H, W, C = input_image.shape H, W, C = input_image.shape
height, width = find_nearest_bucket(H, W, resolution=640) height, width = find_nearest_bucket(H, W, resolution=640)
input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height) 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 = torch.from_numpy(input_image_np).float() / 127.5 - 1
input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None] input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None]
# VAE encoding # VAE encoding
print("VAE-Encoding...") print("VAE-Encoding...")
with open(status_file, "a") as f:
f.write(f"Status: VAE-Encoding\n")
if not high_vram: if not high_vram:
load_model_as_complete(vae, target_device=gpu) load_model_as_complete(vae, target_device=gpu)
@ -174,6 +198,9 @@ def process_cli():
# CLIP Vision # CLIP Vision
print("CLIP Vision-Encoding...") print("CLIP Vision-Encoding...")
with open(status_file, "a") as f:
f.write(f"Status: CLIP Vision-Encoding\n")
if not high_vram: if not high_vram:
load_model_as_complete(image_encoder, target_device=gpu) load_model_as_complete(image_encoder, target_device=gpu)
@ -189,6 +216,9 @@ def process_cli():
# Sampling # Sampling
print("Starte 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) rnd = torch.Generator("cpu").manual_seed(seed)
num_frames = latent_window_size * 4 - 3 num_frames = latent_window_size * 4 - 3
@ -206,6 +236,8 @@ def process_cli():
latent_padding_size = latent_padding * latent_window_size latent_padding_size = latent_padding * latent_window_size
print(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}') 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) 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) 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 current_step = d['i'] + 1
percentage = int(100.0 * current_step / steps) percentage = int(100.0 * current_step / steps)
print(f'Sampling {current_step}/{steps} ({percentage}%)') 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( generated_latents = sample_hunyuan(
transformer=transformer, transformer=transformer,
@ -283,22 +317,32 @@ def process_cli():
if not high_vram: if not high_vram:
unload_complete_models() 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') 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) 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}') 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: if is_last_section:
# Kopiere das finale Video zum gewünschten Ausgabepfad # Kopiere das finale Video zum gewünschten Ausgabepfad
import shutil 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 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: except Exception as e:
traceback.print_exc() 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)}") print(f"Fehler bei der Videogenerierung: {str(e)}")
if not high_vram: if not high_vram:

View File

@ -6,7 +6,7 @@ cd %~dp0webui
"%DIR%\python\python.exe" -m pip install fastapi uvicorn python-multipart "%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 :done
pause pause