2025-07-11 18:02:44 +08:00
import os
import re
import pyJianYingDraft as draft
import shutil
from util import zip_draft , is_windows_path
from oss import upload_to_oss
from typing import Dict , Literal
from draft_cache import DRAFT_CACHE
from save_task_cache import DRAFT_TASKS , get_task_status , update_tasks_cache , update_task_field , increment_task_field , update_task_fields , create_task
from downloader import download_audio , download_file , download_image , download_video
from concurrent . futures import ThreadPoolExecutor , as_completed
import imageio . v2 as imageio
import subprocess
import json
from get_duration_impl import get_video_duration
import uuid
import threading
from collections import OrderedDict
import time
import requests # Import requests for making HTTP calls
import logging
2025-07-13 15:05:20 +08:00
# Import configuration
2025-07-11 18:02:44 +08:00
from settings import IS_CAPCUT_ENV , IS_UPLOAD_DRAFT
2025-07-13 15:05:20 +08:00
# --- Get your Logger instance ---
# The name here must match the logger name you configured in app.py
2025-07-11 18:02:44 +08:00
logger = logging . getLogger ( ' flask_video_generator ' )
2025-07-13 15:05:20 +08:00
# Define task status enumeration type
2025-07-11 18:02:44 +08:00
TaskStatus = Literal [ " initialized " , " processing " , " completed " , " failed " , " not_found " ]
def build_asset_path ( draft_folder : str , draft_id : str , asset_type : str , material_name : str ) - > str :
"""
2025-07-13 15:05:20 +08:00
Build asset file path
: param draft_folder : Draft folder path
: param draft_id : Draft ID
: param asset_type : Asset type ( audio , image , video )
: param material_name : Material name
: return : Built path
2025-07-11 18:02:44 +08:00
"""
if is_windows_path ( draft_folder ) :
if os . name == ' nt ' : # 'nt' for Windows
draft_real_path = os . path . join ( draft_folder , draft_id , " assets " , asset_type , material_name )
else :
windows_drive , windows_path = re . match ( r ' ([a-zA-Z]:)(.*) ' , draft_folder ) . groups ( )
parts = [ p for p in windows_path . split ( ' \\ ' ) if p ]
draft_real_path = os . path . join ( windows_drive , * parts , draft_id , " assets " , asset_type , material_name )
draft_real_path = draft_real_path . replace ( ' / ' , ' \\ ' )
else :
draft_real_path = os . path . join ( draft_folder , draft_id , " assets " , asset_type , material_name )
return draft_real_path
def save_draft_background ( draft_id , draft_folder , task_id ) :
2025-07-13 15:05:20 +08:00
""" Background save draft to OSS """
2025-07-11 18:02:44 +08:00
try :
2025-07-13 15:05:20 +08:00
# Get draft information from global cache
2025-07-11 18:02:44 +08:00
if draft_id not in DRAFT_CACHE :
task_status = {
" status " : " failed " ,
2025-07-13 15:05:20 +08:00
" message " : f " Draft { draft_id } does not exist in cache " ,
2025-07-11 18:02:44 +08:00
" progress " : 0 ,
" completed_files " : 0 ,
" total_files " : 0 ,
" draft_url " : " "
}
2025-07-13 15:05:20 +08:00
update_tasks_cache ( task_id , task_status ) # Use new cache management function
logger . error ( f " Draft { draft_id } does not exist in cache, task { task_id } failed. " )
2025-07-11 18:02:44 +08:00
return
script = DRAFT_CACHE [ draft_id ]
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully retrieved draft { draft_id } from cache. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update task status to processing
2025-07-11 18:02:44 +08:00
task_status = {
" status " : " processing " ,
2025-07-13 15:05:20 +08:00
" message " : " Preparing draft files " ,
2025-07-11 18:02:44 +08:00
" progress " : 0 ,
" completed_files " : 0 ,
" total_files " : 0 ,
" draft_url " : " "
}
2025-07-13 15:05:20 +08:00
update_tasks_cache ( task_id , task_status ) # Use new cache management function
logger . info ( f " Task { task_id } status updated to ' processing ' : Preparing draft files. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Delete possibly existing draft_id folder
2025-07-11 18:02:44 +08:00
if os . path . exists ( draft_id ) :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Deleting existing draft folder (current working directory): { draft_id } " )
2025-07-11 18:02:44 +08:00
shutil . rmtree ( draft_id )
2025-07-13 15:05:20 +08:00
logger . info ( f " Starting to save draft: { draft_id } " )
# Save draft
2025-07-11 18:02:44 +08:00
draft_folder_for_duplicate = draft . Draft_folder ( " ./ " )
2025-07-13 15:05:20 +08:00
# Choose different template directory based on configuration
2025-07-11 18:02:44 +08:00
template_dir = " template " if IS_CAPCUT_ENV else " template_jianying "
draft_folder_for_duplicate . duplicate_as_template ( template_dir , draft_id )
2025-07-13 15:05:20 +08:00
# Update task status
update_task_field ( task_id , " message " , " Updating media file metadata " )
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " progress " , 5 )
2025-07-13 15:05:20 +08:00
logger . info ( f " Task { task_id } progress 5%: Updating media file metadata. " )
2025-07-11 18:02:44 +08:00
update_media_metadata ( script , task_id )
download_tasks = [ ]
audios = script . materials . audios
if audios :
for audio in audios :
remote_url = audio . remote_url
material_name = audio . material_name
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Audio file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add audio download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' audio ' ,
' func ' : download_file ,
' args ' : ( remote_url , f " { draft_id } /assets/audio/ { material_name } " ) ,
' material ' : audio
} )
2025-07-13 15:05:20 +08:00
# Collect video and image download tasks
2025-07-11 18:02:44 +08:00
videos = script . materials . videos
if videos :
for video in videos :
remote_url = video . remote_url
material_name = video . material_name
if video . material_type == ' photo ' :
2025-07-13 15:05:20 +08:00
# Use helper function to build path
2025-07-11 18:02:44 +08:00
if draft_folder :
video . replace_path = build_asset_path ( draft_folder , draft_id , " image " , material_name )
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Image file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add image download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' image ' ,
' func ' : download_file ,
' args ' : ( remote_url , f " { draft_id } /assets/image/ { material_name } " ) ,
' material ' : video
} )
elif video . material_type == ' video ' :
2025-07-13 15:05:20 +08:00
# Use helper function to build path
2025-07-11 18:02:44 +08:00
if draft_folder :
video . replace_path = build_asset_path ( draft_folder , draft_id , " video " , material_name )
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Video file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add video download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' video ' ,
' func ' : download_file ,
' args ' : ( remote_url , f " { draft_id } /assets/video/ { material_name } " ) ,
' material ' : video
} )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , f " Collected { len ( download_tasks ) } download tasks in total " )
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " progress " , 10 )
2025-07-13 15:05:20 +08:00
logger . info ( f " Task { task_id } progress 10%: Collected { len ( download_tasks ) } download tasks in total. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Execute all download tasks concurrently
2025-07-11 18:02:44 +08:00
downloaded_paths = [ ]
completed_files = 0
if download_tasks :
2025-07-13 15:05:20 +08:00
logger . info ( f " Starting concurrent download of { len ( download_tasks ) } files... " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Use thread pool for concurrent downloads, maximum concurrency of 16
2025-07-11 18:02:44 +08:00
with ThreadPoolExecutor ( max_workers = 16 ) as executor :
2025-07-13 15:05:20 +08:00
# Submit all download tasks
2025-07-11 18:02:44 +08:00
future_to_task = {
executor . submit ( task [ ' func ' ] , * task [ ' args ' ] ) : task
for task in download_tasks
}
2025-07-13 15:05:20 +08:00
# Wait for all tasks to complete
2025-07-11 18:02:44 +08:00
for future in as_completed ( future_to_task ) :
task = future_to_task [ future ]
try :
local_path = future . result ( )
downloaded_paths . append ( local_path )
2025-07-13 15:05:20 +08:00
# Update task status - only update completed files count
2025-07-11 18:02:44 +08:00
completed_files + = 1
update_task_field ( task_id , " completed_files " , completed_files )
task_status = get_task_status ( task_id )
completed = task_status [ " completed_files " ]
total = len ( download_tasks )
update_task_field ( task_id , " total_files " , total )
2025-07-13 15:05:20 +08:00
# Download part accounts for 60% of the total progress
2025-07-11 18:02:44 +08:00
download_progress = 10 + int ( ( completed / total ) * 60 )
update_task_field ( task_id , " progress " , download_progress )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , f " Downloaded { completed } / { total } files " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
logger . info ( f " Task { task_id } : Successfully downloaded { task [ ' type ' ] } file, progress { download_progress } . " )
2025-07-11 18:02:44 +08:00
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Task { task_id } : Download { task [ ' type ' ] } file failed: { str ( e ) } " , exc_info = True )
# Continue processing other files, don't interrupt the entire process
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
logger . info ( f " Task { task_id } : Concurrent download completed, downloaded { len ( downloaded_paths ) } files in total. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update task status - Start saving draft information
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " progress " , 70 )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , " Saving draft information " )
logger . info ( f " Task { task_id } progress 70%: Saving draft information. " )
2025-07-11 18:02:44 +08:00
script . dump ( f " { draft_id } /draft_info.json " )
2025-07-13 15:05:20 +08:00
logger . info ( f " Draft information has been saved to { draft_id } /draft_info.json. " )
2025-07-11 18:02:44 +08:00
draft_url = " "
2025-07-13 15:05:20 +08:00
# Only upload draft information when IS_UPLOAD_DRAFT is True
2025-07-11 18:02:44 +08:00
if IS_UPLOAD_DRAFT :
2025-07-13 15:05:20 +08:00
# Update task status - Start compressing draft
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " progress " , 80 )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , " Compressing draft files " )
logger . info ( f " Task { task_id } progress 80%: Compressing draft files. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Compress the entire draft directory
2025-07-11 18:02:44 +08:00
zip_path = zip_draft ( draft_id )
2025-07-13 15:05:20 +08:00
logger . info ( f " Draft directory { draft_id } has been compressed to { zip_path } . " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update task status - Start uploading to OSS
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " progress " , 90 )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , " Uploading to cloud storage " )
logger . info ( f " Task { task_id } progress 90%: Uploading to cloud storage. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Upload to OSS
2025-07-11 18:02:44 +08:00
draft_url = upload_to_oss ( zip_path )
2025-07-13 15:05:20 +08:00
logger . info ( f " Draft archive has been uploaded to OSS, URL: { draft_url } " )
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " draft_url " , draft_url )
2025-07-13 15:05:20 +08:00
# Clean up temporary files
2025-07-11 18:02:44 +08:00
if os . path . exists ( draft_id ) :
shutil . rmtree ( draft_id )
2025-07-13 15:05:20 +08:00
logger . info ( f " Cleaned up temporary draft folder: { draft_id } " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update task status - Completed
2025-07-11 18:02:44 +08:00
update_task_field ( task_id , " status " , " completed " )
update_task_field ( task_id , " progress " , 100 )
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , " Draft creation completed " )
logger . info ( f " Task { task_id } completed, draft URL: { draft_url } " )
2025-07-11 18:02:44 +08:00
return draft_url
except Exception as e :
2025-07-13 15:05:20 +08:00
# Update task status - Failed
2025-07-11 18:02:44 +08:00
update_task_fields ( task_id ,
status = " failed " ,
2025-07-13 15:05:20 +08:00
message = f " Failed to save draft: { str ( e ) } " )
logger . error ( f " Saving draft { draft_id } task { task_id } failed: { str ( e ) } " , exc_info = True )
2025-07-11 18:02:44 +08:00
return " "
def query_task_status ( task_id : str ) :
return get_task_status ( task_id )
def save_draft_impl ( draft_id : str , draft_folder : str = None ) - > Dict [ str , str ] :
2025-07-13 15:05:20 +08:00
""" Start a background task to save the draft """
logger . info ( f " Received save draft request: draft_id= { draft_id } , draft_folder= { draft_folder } " )
2025-07-11 18:02:44 +08:00
try :
2025-07-13 15:05:20 +08:00
# Generate a unique task ID
2025-07-11 18:02:44 +08:00
task_id = draft_id
create_task ( task_id )
2025-07-13 15:05:20 +08:00
logger . info ( f " Task { task_id } has been created. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Changed to synchronous execution
2025-07-11 18:02:44 +08:00
return {
" success " : True ,
" draft_url " : save_draft_background ( draft_id , draft_folder , task_id )
}
2025-07-13 15:05:20 +08:00
# # Start a background thread to execute the task
2025-07-11 18:02:44 +08:00
# thread = threading.Thread(
# target=save_draft_background,
# args=(draft_id, draft_folder, task_id)
# )
# thread.start()
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Failed to start save draft task { draft_id } : { str ( e ) } " , exc_info = True )
2025-07-11 18:02:44 +08:00
return {
" success " : False ,
" error " : str ( e )
}
def update_media_metadata ( script , task_id = None ) :
"""
2025-07-13 15:05:20 +08:00
Update metadata for all media files in the script ( duration , width / height , etc . )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
: param script : Draft script object
: param task_id : Optional task ID for updating task status
2025-07-11 18:02:44 +08:00
: return : None
"""
2025-07-13 15:05:20 +08:00
# Process audio file metadata
2025-07-11 18:02:44 +08:00
audios = script . materials . audios
if not audios :
2025-07-13 15:05:20 +08:00
logger . info ( " No audio files found in the draft. " )
2025-07-11 18:02:44 +08:00
else :
for audio in audios :
remote_url = audio . remote_url
material_name = audio . material_name
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Audio file { material_name } has no remote_url, skipped. " )
2025-07-11 18:02:44 +08:00
continue
try :
video_command = [
' ffprobe ' ,
' -v ' , ' error ' ,
' -select_streams ' , ' v:0 ' ,
' -show_entries ' , ' stream=codec_type ' ,
' -of ' , ' json ' ,
remote_url
]
video_result = subprocess . check_output ( video_command , stderr = subprocess . STDOUT )
video_result_str = video_result . decode ( ' utf-8 ' )
2025-07-13 15:05:20 +08:00
# Find JSON start position (first '{')
2025-07-11 18:02:44 +08:00
video_json_start = video_result_str . find ( ' { ' )
if video_json_start != - 1 :
video_json_str = video_result_str [ video_json_start : ]
video_info = json . loads ( video_json_str )
if ' streams ' in video_info and len ( video_info [ ' streams ' ] ) > 0 :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Audio file { material_name } contains video tracks, skipped its metadata update. " )
2025-07-11 18:02:44 +08:00
continue
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Error occurred while checking if audio { material_name } contains video streams: { str ( e ) } " , exc_info = True )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Get audio duration and set it
2025-07-11 18:02:44 +08:00
try :
duration_result = get_video_duration ( remote_url )
if duration_result [ " success " ] :
if task_id :
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , f " Processing audio metadata: { material_name } " )
# Convert seconds to microseconds
2025-07-11 18:02:44 +08:00
audio . duration = int ( duration_result [ " output " ] * 1000000 )
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully obtained audio { material_name } duration: { duration_result [ ' output ' ] : .2f } seconds ( { audio . duration } microseconds). " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update timerange for all segments using this audio material
2025-07-11 18:02:44 +08:00
for track_name , track in script . tracks . items ( ) :
if track . track_type == draft . Track_type . audio :
for segment in track . segments :
if isinstance ( segment , draft . Audio_segment ) and segment . material_id == audio . material_id :
2025-07-13 15:05:20 +08:00
# Get current settings
2025-07-11 18:02:44 +08:00
current_target = segment . target_timerange
current_source = segment . source_timerange
speed = segment . speed . speed
2025-07-13 15:05:20 +08:00
# If the end time of source_timerange exceeds the new audio duration, adjust it
2025-07-11 18:02:44 +08:00
if current_source . end > audio . duration or current_source . end < = 0 :
2025-07-13 15:05:20 +08:00
# Adjust source_timerange to fit the new audio duration
2025-07-11 18:02:44 +08:00
new_source_duration = audio . duration - current_source . start
if new_source_duration < = 0 :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Audio segment { segment . segment_id } start time { current_source . start } exceeds audio duration { audio . duration } , will skip this segment. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Update source_timerange
2025-07-11 18:02:44 +08:00
segment . source_timerange = draft . Timerange ( current_source . start , new_source_duration )
2025-07-13 15:05:20 +08:00
# Update target_timerange based on new source_timerange and speed
2025-07-11 18:02:44 +08:00
new_target_duration = int ( new_source_duration / speed )
segment . target_timerange = draft . Timerange ( current_target . start , new_target_duration )
2025-07-13 15:05:20 +08:00
logger . info ( f " Adjusted audio segment { segment . segment_id } timerange to fit the new audio duration. " )
2025-07-11 18:02:44 +08:00
else :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Unable to get audio { material_name } duration: { duration_result [ ' error ' ] } . " )
2025-07-11 18:02:44 +08:00
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Error occurred while getting audio { material_name } duration: { str ( e ) } " , exc_info = True )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Process video and image file metadata
2025-07-11 18:02:44 +08:00
videos = script . materials . videos
if not videos :
2025-07-13 15:05:20 +08:00
logger . info ( " No video or image files found in the draft. " )
2025-07-11 18:02:44 +08:00
else :
for video in videos :
remote_url = video . remote_url
material_name = video . material_name
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Media file { material_name } has no remote_url, skipped. " )
2025-07-11 18:02:44 +08:00
continue
if video . material_type == ' photo ' :
2025-07-13 15:05:20 +08:00
# Use imageio to get image width/height and set it
2025-07-11 18:02:44 +08:00
try :
if task_id :
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , f " Processing image metadata: { material_name } " )
2025-07-11 18:02:44 +08:00
img = imageio . imread ( remote_url )
video . height , video . width = img . shape [ : 2 ]
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully set image { material_name } dimensions: { video . width } x { video . height } . " )
2025-07-11 18:02:44 +08:00
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Failed to set image { material_name } dimensions: { str ( e ) } , using default values 1920x1080. " , exc_info = True )
2025-07-11 18:02:44 +08:00
video . width = 1920
video . height = 1080
elif video . material_type == ' video ' :
2025-07-13 15:05:20 +08:00
# Get video duration and width/height information
2025-07-11 18:02:44 +08:00
try :
if task_id :
2025-07-13 15:05:20 +08:00
update_task_field ( task_id , " message " , f " Processing video metadata: { material_name } " )
# Use ffprobe to get video information
2025-07-11 18:02:44 +08:00
command = [
' ffprobe ' ,
' -v ' , ' error ' ,
2025-07-13 15:05:20 +08:00
' -select_streams ' , ' v:0 ' , # Select the first video stream
2025-07-11 18:02:44 +08:00
' -show_entries ' , ' stream=width,height,duration ' ,
' -show_entries ' , ' format=duration ' ,
' -of ' , ' json ' ,
remote_url
]
result = subprocess . check_output ( command , stderr = subprocess . STDOUT )
result_str = result . decode ( ' utf-8 ' )
2025-07-13 15:05:20 +08:00
# Find JSON start position (first '{')
2025-07-11 18:02:44 +08:00
json_start = result_str . find ( ' { ' )
if json_start != - 1 :
json_str = result_str [ json_start : ]
info = json . loads ( json_str )
if ' streams ' in info and len ( info [ ' streams ' ] ) > 0 :
stream = info [ ' streams ' ] [ 0 ]
2025-07-13 15:05:20 +08:00
# Set width and height
2025-07-11 18:02:44 +08:00
video . width = int ( stream . get ( ' width ' , 0 ) )
video . height = int ( stream . get ( ' height ' , 0 ) )
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully set video { material_name } dimensions: { video . width } x { video . height } . " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Set duration
# Prefer stream duration, if not available use format duration
2025-07-11 18:02:44 +08:00
duration = stream . get ( ' duration ' ) or info [ ' format ' ] . get ( ' duration ' , ' 0 ' )
2025-07-13 15:05:20 +08:00
video . duration = int ( float ( duration ) * 1000000 ) # Convert to microseconds
logger . info ( f " Successfully obtained video { material_name } duration: { float ( duration ) : .2f } seconds ( { video . duration } microseconds). " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Update timerange for all segments using this video material
2025-07-11 18:02:44 +08:00
for track_name , track in script . tracks . items ( ) :
if track . track_type == draft . Track_type . video :
for segment in track . segments :
if isinstance ( segment , draft . Video_segment ) and segment . material_id == video . material_id :
2025-07-13 15:05:20 +08:00
# Get current settings
2025-07-11 18:02:44 +08:00
current_target = segment . target_timerange
current_source = segment . source_timerange
speed = segment . speed . speed
2025-07-13 15:05:20 +08:00
# If the end time of source_timerange exceeds the new video duration, adjust it
2025-07-11 18:02:44 +08:00
if current_source . end > video . duration or current_source . end < = 0 :
2025-07-13 15:05:20 +08:00
# Adjust source_timerange to fit the new video duration
2025-07-11 18:02:44 +08:00
new_source_duration = video . duration - current_source . start
if new_source_duration < = 0 :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Video segment { segment . segment_id } start time { current_source . start } exceeds video duration { video . duration } , will skip this segment. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Update source_timerange
2025-07-11 18:02:44 +08:00
segment . source_timerange = draft . Timerange ( current_source . start , new_source_duration )
2025-07-13 15:05:20 +08:00
# Update target_timerange based on new source_timerange and speed
2025-07-11 18:02:44 +08:00
new_target_duration = int ( new_source_duration / speed )
segment . target_timerange = draft . Timerange ( current_target . start , new_target_duration )
2025-07-13 15:05:20 +08:00
logger . info ( f " Adjusted video segment { segment . segment_id } timerange to fit the new video duration. " )
2025-07-11 18:02:44 +08:00
else :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Unable to get video { material_name } stream information. " )
# Set default values
2025-07-11 18:02:44 +08:00
video . width = 1920
video . height = 1080
else :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Could not find JSON data in ffprobe output. " )
# Set default values
2025-07-11 18:02:44 +08:00
video . width = 1920
video . height = 1080
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Error occurred while getting video { material_name } information: { str ( e ) } , using default values 1920x1080. " , exc_info = True )
# Set default values
2025-07-11 18:02:44 +08:00
video . width = 1920
video . height = 1080
2025-07-13 15:05:20 +08:00
# Try to get duration separately
2025-07-11 18:02:44 +08:00
try :
duration_result = get_video_duration ( remote_url )
if duration_result [ " success " ] :
2025-07-13 15:05:20 +08:00
# Convert seconds to microseconds
2025-07-11 18:02:44 +08:00
video . duration = int ( duration_result [ " output " ] * 1000000 )
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully obtained video { material_name } duration: { duration_result [ ' output ' ] : .2f } seconds ( { video . duration } microseconds). " )
2025-07-11 18:02:44 +08:00
else :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Warning: Unable to get video { material_name } duration: { duration_result [ ' error ' ] } . " )
2025-07-11 18:02:44 +08:00
except Exception as e2 :
2025-07-13 15:05:20 +08:00
logger . error ( f " Error occurred while getting video { material_name } duration: { str ( e2 ) } . " , exc_info = True )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# After updating all segments' timerange, check if there are time range conflicts in each track, and delete the later segment in case of conflict
logger . info ( " Checking track segment time range conflicts... " )
2025-07-11 18:02:44 +08:00
for track_name , track in script . tracks . items ( ) :
2025-07-13 15:05:20 +08:00
# Use a set to record segment indices that need to be deleted
2025-07-11 18:02:44 +08:00
to_remove = set ( )
2025-07-13 15:05:20 +08:00
# Check for conflicts between all segments
2025-07-11 18:02:44 +08:00
for i in range ( len ( track . segments ) ) :
2025-07-13 15:05:20 +08:00
# Skip if current segment is already marked for deletion
2025-07-11 18:02:44 +08:00
if i in to_remove :
continue
for j in range ( len ( track . segments ) ) :
2025-07-13 15:05:20 +08:00
# Skip self-comparison and segments already marked for deletion
2025-07-11 18:02:44 +08:00
if i == j or j in to_remove :
continue
2025-07-13 15:05:20 +08:00
# Check if there is a conflict
2025-07-11 18:02:44 +08:00
if track . segments [ i ] . overlaps ( track . segments [ j ] ) :
2025-07-13 15:05:20 +08:00
# Always keep the segment with the smaller index (added first)
2025-07-11 18:02:44 +08:00
later_index = max ( i , j )
2025-07-13 15:05:20 +08:00
logger . warning ( f " Time range conflict between segments { track . segments [ min ( i , j ) ] . segment_id } and { track . segments [ later_index ] . segment_id } in track { track_name } , deleting the later segment " )
2025-07-11 18:02:44 +08:00
to_remove . add ( later_index )
2025-07-13 15:05:20 +08:00
# Delete marked segments from back to front to avoid index change issues
2025-07-11 18:02:44 +08:00
for index in sorted ( to_remove , reverse = True ) :
track . segments . pop ( index )
2025-07-13 15:05:20 +08:00
# After updating all segments' timerange, recalculate the total duration of the script
2025-07-11 18:02:44 +08:00
max_duration = 0
for track_name , track in script . tracks . items ( ) :
for segment in track . segments :
max_duration = max ( max_duration , segment . end )
script . duration = max_duration
2025-07-13 15:05:20 +08:00
logger . info ( f " Updated script total duration to: { script . duration } microseconds. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Process all pending keyframes in tracks
logger . info ( " Processing pending keyframes... " )
2025-07-11 18:02:44 +08:00
for track_name , track in script . tracks . items ( ) :
if hasattr ( track , ' pending_keyframes ' ) and track . pending_keyframes :
2025-07-13 15:05:20 +08:00
logger . info ( f " Processing { len ( track . pending_keyframes ) } pending keyframes in track { track_name } ... " )
2025-07-11 18:02:44 +08:00
track . process_pending_keyframes ( )
2025-07-13 15:05:20 +08:00
logger . info ( f " Pending keyframes in track { track_name } have been processed. " )
2025-07-11 18:02:44 +08:00
def query_script_impl ( draft_id : str , force_update : bool = True ) :
"""
2025-07-13 15:05:20 +08:00
Query draft script object , with option to force refresh media metadata
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
: param draft_id : Draft ID
: param force_update : Whether to force refresh media metadata , default is True
: return : Script object
2025-07-11 18:02:44 +08:00
"""
2025-07-13 15:05:20 +08:00
# Get draft information from global cache
2025-07-11 18:02:44 +08:00
if draft_id not in DRAFT_CACHE :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Draft { draft_id } does not exist in cache. " )
2025-07-11 18:02:44 +08:00
return None
script = DRAFT_CACHE [ draft_id ]
2025-07-13 15:05:20 +08:00
logger . info ( f " Retrieved draft { draft_id } from cache. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# If force_update is True, force refresh media metadata
2025-07-11 18:02:44 +08:00
if force_update :
2025-07-13 15:05:20 +08:00
logger . info ( f " Force refreshing media metadata for draft { draft_id } . " )
2025-07-11 18:02:44 +08:00
update_media_metadata ( script )
2025-07-13 15:05:20 +08:00
# Return script object
2025-07-11 18:02:44 +08:00
return script
def download_script ( draft_id : str , draft_folder : str = None , script_data : Dict = None ) - > Dict [ str , str ] :
2025-07-13 15:05:20 +08:00
""" Downloads the draft script and its associated media assets.
2025-07-11 18:02:44 +08:00
This function fetches the script object from a remote API ,
then iterates through its materials ( audios , videos , images )
to download them to the specified draft folder . It also updates
task status and progress throughout the process .
: param draft_id : The ID of the draft to download .
: param draft_folder : The base folder where the draft ' s assets will be stored.
If None , assets will be stored directly under a folder named
after the draft_id in the current working directory .
: return : A dictionary indicating success and , if successful , the URL where the draft
would eventually be saved ( though this function primarily focuses on download ) .
If failed , it returns an error message .
"""
2025-07-13 15:05:20 +08:00
logger . info ( f " Starting to download draft: { draft_id } to folder: { draft_folder } " )
# Copy template to target directory
2025-07-11 18:02:44 +08:00
template_path = os . path . join ( " ./ " , ' template ' ) if IS_CAPCUT_ENV else os . path . join ( " ./ " , ' template_jianying ' )
new_draft_path = os . path . join ( draft_folder , draft_id )
if os . path . exists ( new_draft_path ) :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Deleting existing draft target folder: { new_draft_path } " )
2025-07-11 18:02:44 +08:00
shutil . rmtree ( new_draft_path )
2025-07-13 15:05:20 +08:00
# Copy draft folder
2025-07-11 18:02:44 +08:00
shutil . copytree ( template_path , new_draft_path )
try :
# 1. Fetch the script from the remote endpoint
if script_data is None :
query_url = " https://cut-jianying-vdvswivepm.cn-hongkong.fcapp.run/query_script "
headers = { " Content-Type " : " application/json " }
payload = { " draft_id " : draft_id }
2025-07-13 15:05:20 +08:00
logger . info ( f " Attempting to get script for draft ID: { draft_id } from { query_url } . " )
2025-07-11 18:02:44 +08:00
response = requests . post ( query_url , headers = headers , json = payload )
response . raise_for_status ( ) # Raise an exception for HTTP errors (4xx or 5xx)
script_data = json . loads ( response . json ( ) . get ( ' output ' ) )
2025-07-13 15:05:20 +08:00
logger . info ( f " Successfully retrieved script data for draft { draft_id } . " )
2025-07-11 18:02:44 +08:00
else :
2025-07-13 15:05:20 +08:00
logger . info ( f " Using provided script_data, skipping remote retrieval. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Collect download tasks
2025-07-11 18:02:44 +08:00
download_tasks = [ ]
2025-07-13 15:05:20 +08:00
# Collect audio download tasks
2025-07-11 18:02:44 +08:00
audios = script_data . get ( ' materials ' , { } ) . get ( ' audios ' , [ ] )
if audios :
for audio in audios :
remote_url = audio [ ' remote_url ' ]
material_name = audio [ ' name ' ]
2025-07-13 15:05:20 +08:00
# Use helper function to build path
2025-07-11 18:02:44 +08:00
if draft_folder :
audio [ ' path ' ] = build_asset_path ( draft_folder , draft_id , " audio " , material_name )
2025-07-13 15:05:20 +08:00
logger . debug ( f " Local path for audio { material_name } : { audio [ ' path ' ] } " )
2025-07-11 18:02:44 +08:00
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Audio file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add audio download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' audio ' ,
' func ' : download_file ,
' args ' : ( remote_url , audio [ ' path ' ] ) ,
' material ' : audio
} )
2025-07-13 15:05:20 +08:00
# Collect video and image download tasks
2025-07-11 18:02:44 +08:00
videos = script_data [ ' materials ' ] [ ' videos ' ]
if videos :
for video in videos :
remote_url = video [ ' remote_url ' ]
material_name = video [ ' material_name ' ]
if video [ ' type ' ] == ' photo ' :
2025-07-13 15:05:20 +08:00
# Use helper function to build path
2025-07-11 18:02:44 +08:00
if draft_folder :
video [ ' path ' ] = build_asset_path ( draft_folder , draft_id , " image " , material_name )
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Image file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add image download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' image ' ,
' func ' : download_file ,
' args ' : ( remote_url , video [ ' path ' ] ) ,
' material ' : video
} )
elif video [ ' type ' ] == ' video ' :
2025-07-13 15:05:20 +08:00
# Use helper function to build path
2025-07-11 18:02:44 +08:00
if draft_folder :
video [ ' path ' ] = build_asset_path ( draft_folder , draft_id , " video " , material_name )
if not remote_url :
2025-07-13 15:05:20 +08:00
logger . warning ( f " Video file { material_name } has no remote_url, skipping download. " )
2025-07-11 18:02:44 +08:00
continue
2025-07-13 15:05:20 +08:00
# Add video download task
2025-07-11 18:02:44 +08:00
download_tasks . append ( {
' type ' : ' video ' ,
' func ' : download_file ,
' args ' : ( remote_url , video [ ' path ' ] ) ,
' material ' : video
} )
2025-07-13 15:05:20 +08:00
# Execute all download tasks concurrently
2025-07-11 18:02:44 +08:00
downloaded_paths = [ ]
completed_files = 0
if download_tasks :
2025-07-13 15:05:20 +08:00
logger . info ( f " Starting concurrent download of { len ( download_tasks ) } files... " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
# Use thread pool for concurrent downloads, maximum concurrency of 16
2025-07-11 18:02:44 +08:00
with ThreadPoolExecutor ( max_workers = 16 ) as executor :
2025-07-13 15:05:20 +08:00
# Submit all download tasks
2025-07-11 18:02:44 +08:00
future_to_task = {
executor . submit ( task [ ' func ' ] , * task [ ' args ' ] ) : task
for task in download_tasks
}
2025-07-13 15:05:20 +08:00
# Wait for all tasks to complete
2025-07-11 18:02:44 +08:00
for future in as_completed ( future_to_task ) :
task = future_to_task [ future ]
try :
local_path = future . result ( )
downloaded_paths . append ( local_path )
2025-07-13 15:05:20 +08:00
# Update task status - only update completed files count
2025-07-11 18:02:44 +08:00
completed_files + = 1
2025-07-13 15:05:20 +08:00
logger . info ( f " Downloaded { completed_files } / { len ( download_tasks ) } files. " )
2025-07-11 18:02:44 +08:00
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Failed to download { task [ ' type ' ] } file { task [ ' args ' ] [ 0 ] } : { str ( e ) } " , exc_info = True )
logger . error ( " Download failed. " )
# Continue processing other files, don't interrupt the entire process
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
logger . info ( f " Concurrent download completed, downloaded { len ( downloaded_paths ) } files in total. " )
2025-07-11 18:02:44 +08:00
2025-07-13 15:05:20 +08:00
""" Write draft file content to file """
2025-07-11 18:02:44 +08:00
with open ( f " { draft_folder } / { draft_id } /draft_info.json " , " w " , encoding = " utf-8 " ) as f :
f . write ( json . dumps ( script_data ) )
2025-07-13 15:05:20 +08:00
logger . info ( f " Draft has been saved. " )
2025-07-11 18:02:44 +08:00
# No draft_url for download, but return success
return { " success " : True , " message " : f " Draft { draft_id } and its assets downloaded successfully " }
except requests . exceptions . RequestException as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " API request failed: { e } " , exc_info = True )
2025-07-11 18:02:44 +08:00
return { " success " : False , " error " : f " Failed to fetch script from API: { str ( e ) } " }
except Exception as e :
2025-07-13 15:05:20 +08:00
logger . error ( f " Unexpected error during download: { e } " , exc_info = True )
2025-07-11 18:02:44 +08:00
return { " success " : False , " error " : f " An unexpected error occurred: { str ( e ) } " }
if __name__ == " __main__ " :
print ( ' hello ' )
download_script ( " dfd_cat_1751012163_a7e8c315 " , ' /Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft ' )