Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -88,6 +88,23 @@ st.markdown("""
|
|
| 88 |
</style>
|
| 89 |
""", unsafe_allow_html=True)
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def extract_dominant_colors(image, n_colors=3):
|
| 92 |
"""Extract dominant neon-suitable colors from the image"""
|
| 93 |
# Resize for faster processing
|
|
@@ -247,7 +264,7 @@ def create_enhanced_neon_glow(edge_image, colors, glow_size=20):
|
|
| 247 |
result = np.clip(result, 0, 255).astype(np.uint8)
|
| 248 |
return result
|
| 249 |
|
| 250 |
-
def create_human_like_drawing(image, edges, strokes, num_frames, colors, glow_size=20, bg_color=(0, 0, 0), hold_drawn_frames=0):
|
| 251 |
"""Create drawing animation that progressively reveals the original image with accurate colors"""
|
| 252 |
height, width = edges.shape
|
| 253 |
frames = []
|
|
@@ -349,7 +366,7 @@ def create_human_like_drawing(image, edges, strokes, num_frames, colors, glow_si
|
|
| 349 |
|
| 350 |
# Add final complete frame - show 100% original image
|
| 351 |
final_frame = image.copy()
|
| 352 |
-
frames.extend([final_frame] * 25) # Hold for
|
| 353 |
|
| 354 |
gc.collect()
|
| 355 |
return frames
|
|
@@ -467,23 +484,50 @@ def create_outro_frame(text, width, height, bg_color=(10, 10, 15),
|
|
| 467 |
|
| 468 |
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
|
| 469 |
|
| 470 |
-
def add_audio_to_video(video_path, audio_path, output_path):
|
| 471 |
-
"""Add audio to video using ffmpeg"""
|
| 472 |
import subprocess
|
| 473 |
|
| 474 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
cmd = [
|
| 476 |
'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error',
|
| 477 |
'-i', video_path,
|
|
|
|
| 478 |
'-i', audio_path,
|
| 479 |
-
'-c:v', '
|
| 480 |
-
'-preset', 'fast',
|
| 481 |
'-c:a', 'aac',
|
| 482 |
'-b:a', '192k',
|
| 483 |
-
'-shortest',
|
| 484 |
-
output_path
|
| 485 |
]
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
if result.returncode != 0:
|
| 488 |
st.warning(f"Audio mixing warning: {result.stderr}")
|
| 489 |
return False
|
|
@@ -491,9 +535,13 @@ def add_audio_to_video(video_path, audio_path, output_path):
|
|
| 491 |
except FileNotFoundError:
|
| 492 |
st.error("FFmpeg not found. Please install FFmpeg to add audio.")
|
| 493 |
return False
|
|
|
|
|
|
|
|
|
|
| 494 |
except Exception as e:
|
| 495 |
st.error(f"Audio error: {str(e)}")
|
| 496 |
return False
|
|
|
|
| 497 |
|
| 498 |
def create_video(frames, fps, output_path, aspect_ratio):
|
| 499 |
"""Create video from frames"""
|
|
@@ -547,7 +595,15 @@ with col1:
|
|
| 547 |
|
| 548 |
if uploaded_file:
|
| 549 |
image = Image.open(uploaded_file)
|
| 550 |
-
st.image(image, caption="Original Image")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
|
| 552 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 553 |
|
|
@@ -557,7 +613,12 @@ with col2:
|
|
| 557 |
|
| 558 |
# Simple settings
|
| 559 |
duration = st.slider("Animation Duration (seconds)", 5, 60, 10)
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
|
| 562 |
st.info("✨ Colors and effects are automatically detected from your image!")
|
| 563 |
|
|
@@ -578,7 +639,35 @@ col6, col7 = st.columns(2)
|
|
| 578 |
|
| 579 |
with col6:
|
| 580 |
aspect_ratio = st.selectbox("Aspect Ratio", ["16:9", "9:16", "4:5", "1:1"])
|
| 581 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
|
| 583 |
with col7:
|
| 584 |
fps = st.slider("Frame Rate (FPS)", 24, 60, 30)
|
|
@@ -661,11 +750,13 @@ if st.button("🎬 Generate Neon Drawing Video", type="primary"):
|
|
| 661 |
progress_bar.progress(55)
|
| 662 |
|
| 663 |
hold_drawn_frames = int(hold_drawn * fps)
|
|
|
|
| 664 |
|
| 665 |
frames = create_human_like_drawing(
|
| 666 |
image_cv, edges, strokes, num_frames,
|
| 667 |
colors=neon_colors, glow_size=glow_intensity,
|
| 668 |
-
bg_color=bg_color, hold_drawn_frames=hold_drawn_frames
|
|
|
|
| 669 |
)
|
| 670 |
|
| 671 |
if not frames:
|
|
@@ -725,7 +816,9 @@ if st.button("🎬 Generate Neon Drawing Video", type="primary"):
|
|
| 725 |
final_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 726 |
final_video.close()
|
| 727 |
|
| 728 |
-
if add_audio_to_video(video_path, temp_audio.name, final_video.name
|
|
|
|
|
|
|
| 729 |
final_video_path = final_video.name
|
| 730 |
try:
|
| 731 |
os.unlink(video_path)
|
|
|
|
| 88 |
</style>
|
| 89 |
""", unsafe_allow_html=True)
|
| 90 |
|
| 91 |
+
def detect_best_aspect_ratio(image):
|
| 92 |
+
"""Detect the best aspect ratio for the image"""
|
| 93 |
+
height, width = image.shape[:2]
|
| 94 |
+
current_ratio = width / height
|
| 95 |
+
|
| 96 |
+
ratios = {
|
| 97 |
+
"16:9": 16/9,
|
| 98 |
+
"9:16": 9/16,
|
| 99 |
+
"4:5": 4/5,
|
| 100 |
+
"1:1": 1
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# Find closest ratio
|
| 104 |
+
best_ratio = min(ratios.items(), key=lambda x: abs(x[1] - current_ratio))
|
| 105 |
+
|
| 106 |
+
return best_ratio[0], current_ratio
|
| 107 |
+
|
| 108 |
def extract_dominant_colors(image, n_colors=3):
|
| 109 |
"""Extract dominant neon-suitable colors from the image"""
|
| 110 |
# Resize for faster processing
|
|
|
|
| 264 |
result = np.clip(result, 0, 255).astype(np.uint8)
|
| 265 |
return result
|
| 266 |
|
| 267 |
+
def create_human_like_drawing(image, edges, strokes, num_frames, colors, glow_size=20, bg_color=(0, 0, 0), hold_drawn_frames=0, hold_final_frames=0):
|
| 268 |
"""Create drawing animation that progressively reveals the original image with accurate colors"""
|
| 269 |
height, width = edges.shape
|
| 270 |
frames = []
|
|
|
|
| 366 |
|
| 367 |
# Add final complete frame - show 100% original image
|
| 368 |
final_frame = image.copy()
|
| 369 |
+
frames.extend([final_frame] * max(hold_final_frames, 25)) # Hold for specified frames or minimum 25
|
| 370 |
|
| 371 |
gc.collect()
|
| 372 |
return frames
|
|
|
|
| 484 |
|
| 485 |
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
|
| 486 |
|
| 487 |
+
def add_audio_to_video(video_path, audio_path, output_path, start_time=0.0, fadeout_duration=2.0):
|
| 488 |
+
"""Add audio to video using ffmpeg with start time and fade out"""
|
| 489 |
import subprocess
|
| 490 |
|
| 491 |
try:
|
| 492 |
+
# Build ffmpeg command with audio filters
|
| 493 |
+
audio_filters = []
|
| 494 |
+
|
| 495 |
+
# Add fade out filter
|
| 496 |
+
if fadeout_duration > 0:
|
| 497 |
+
# Get video duration to calculate fade start
|
| 498 |
+
probe_cmd = [
|
| 499 |
+
'ffprobe', '-v', 'error', '-show_entries',
|
| 500 |
+
'format=duration', '-of',
|
| 501 |
+
'default=noprint_wrappers=1:nokey=1', video_path
|
| 502 |
+
]
|
| 503 |
+
try:
|
| 504 |
+
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=10)
|
| 505 |
+
video_duration = float(result.stdout.strip())
|
| 506 |
+
fade_start = max(0, video_duration - fadeout_duration)
|
| 507 |
+
audio_filters.append(f"afade=t=out:st={fade_start}:d={fadeout_duration}")
|
| 508 |
+
except:
|
| 509 |
+
# If can't get duration, use default fade
|
| 510 |
+
audio_filters.append(f"afade=t=out:d={fadeout_duration}")
|
| 511 |
+
|
| 512 |
+
# Combine filters
|
| 513 |
+
filter_str = ",".join(audio_filters) if audio_filters else None
|
| 514 |
+
|
| 515 |
cmd = [
|
| 516 |
'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error',
|
| 517 |
'-i', video_path,
|
| 518 |
+
'-ss', str(start_time), # Start audio from this time
|
| 519 |
'-i', audio_path,
|
| 520 |
+
'-c:v', 'copy', # Copy video without re-encoding
|
|
|
|
| 521 |
'-c:a', 'aac',
|
| 522 |
'-b:a', '192k',
|
|
|
|
|
|
|
| 523 |
]
|
| 524 |
+
|
| 525 |
+
if filter_str:
|
| 526 |
+
cmd.extend(['-af', filter_str])
|
| 527 |
+
|
| 528 |
+
cmd.extend(['-shortest', output_path])
|
| 529 |
+
|
| 530 |
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
| 531 |
if result.returncode != 0:
|
| 532 |
st.warning(f"Audio mixing warning: {result.stderr}")
|
| 533 |
return False
|
|
|
|
| 535 |
except FileNotFoundError:
|
| 536 |
st.error("FFmpeg not found. Please install FFmpeg to add audio.")
|
| 537 |
return False
|
| 538 |
+
except subprocess.TimeoutExpired:
|
| 539 |
+
st.error("Audio processing timeout. Try a shorter audio file.")
|
| 540 |
+
return False
|
| 541 |
except Exception as e:
|
| 542 |
st.error(f"Audio error: {str(e)}")
|
| 543 |
return False
|
| 544 |
+
|
| 545 |
|
| 546 |
def create_video(frames, fps, output_path, aspect_ratio):
|
| 547 |
"""Create video from frames"""
|
|
|
|
| 595 |
|
| 596 |
if uploaded_file:
|
| 597 |
image = Image.open(uploaded_file)
|
| 598 |
+
st.image(image, caption="Original Image", use_container_width=True)
|
| 599 |
+
|
| 600 |
+
# Auto-detect best aspect ratio
|
| 601 |
+
image_array = np.array(image)
|
| 602 |
+
image_cv = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
|
| 603 |
+
best_ratio, current_ratio = detect_best_aspect_ratio(image_cv)
|
| 604 |
+
|
| 605 |
+
st.success(f"📐 **Recommended Aspect Ratio:** {best_ratio}")
|
| 606 |
+
st.info(f"ℹ️ Current image ratio: {current_ratio:.2f}:1")
|
| 607 |
|
| 608 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 609 |
|
|
|
|
| 613 |
|
| 614 |
# Simple settings
|
| 615 |
duration = st.slider("Animation Duration (seconds)", 5, 60, 10)
|
| 616 |
+
|
| 617 |
+
col_hold1, col_hold2 = st.columns(2)
|
| 618 |
+
with col_hold1:
|
| 619 |
+
hold_drawn = st.slider("Hold Drawn Image (sec)", 0, 10, 3)
|
| 620 |
+
with col_hold2:
|
| 621 |
+
hold_final = st.slider("Hold Final Image (sec)", 0, 10, 2)
|
| 622 |
|
| 623 |
st.info("✨ Colors and effects are automatically detected from your image!")
|
| 624 |
|
|
|
|
| 639 |
|
| 640 |
with col6:
|
| 641 |
aspect_ratio = st.selectbox("Aspect Ratio", ["16:9", "9:16", "4:5", "1:1"])
|
| 642 |
+
|
| 643 |
+
st.markdown("---")
|
| 644 |
+
st.subheader("🎵 Background Audio")
|
| 645 |
+
audio_file = st.file_uploader("Upload Audio (Optional)", type=['mp3', 'wav', 'ogg', 'm4a'])
|
| 646 |
+
|
| 647 |
+
if audio_file:
|
| 648 |
+
# Audio preview
|
| 649 |
+
st.audio(audio_file, format=f'audio/{audio_file.name.split(".")[-1]}')
|
| 650 |
+
|
| 651 |
+
# Audio controls
|
| 652 |
+
col_audio1, col_audio2 = st.columns(2)
|
| 653 |
+
with col_audio1:
|
| 654 |
+
audio_start_time = st.number_input(
|
| 655 |
+
"Start Time (seconds)",
|
| 656 |
+
min_value=0.0,
|
| 657 |
+
max_value=300.0,
|
| 658 |
+
value=0.0,
|
| 659 |
+
step=0.5,
|
| 660 |
+
help="Audio will start from this time"
|
| 661 |
+
)
|
| 662 |
+
with col_audio2:
|
| 663 |
+
audio_fadeout = st.number_input(
|
| 664 |
+
"Fade Out Duration (sec)",
|
| 665 |
+
min_value=0.0,
|
| 666 |
+
max_value=10.0,
|
| 667 |
+
value=2.0,
|
| 668 |
+
step=0.5,
|
| 669 |
+
help="Smooth fade out at the end"
|
| 670 |
+
)
|
| 671 |
|
| 672 |
with col7:
|
| 673 |
fps = st.slider("Frame Rate (FPS)", 24, 60, 30)
|
|
|
|
| 750 |
progress_bar.progress(55)
|
| 751 |
|
| 752 |
hold_drawn_frames = int(hold_drawn * fps)
|
| 753 |
+
hold_final_frames = int(hold_final * fps)
|
| 754 |
|
| 755 |
frames = create_human_like_drawing(
|
| 756 |
image_cv, edges, strokes, num_frames,
|
| 757 |
colors=neon_colors, glow_size=glow_intensity,
|
| 758 |
+
bg_color=bg_color, hold_drawn_frames=hold_drawn_frames,
|
| 759 |
+
hold_final_frames=hold_final_frames
|
| 760 |
)
|
| 761 |
|
| 762 |
if not frames:
|
|
|
|
| 816 |
final_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 817 |
final_video.close()
|
| 818 |
|
| 819 |
+
if add_audio_to_video(video_path, temp_audio.name, final_video.name,
|
| 820 |
+
start_time=audio_start_time,
|
| 821 |
+
fadeout_duration=audio_fadeout):
|
| 822 |
final_video_path = final_video.name
|
| 823 |
try:
|
| 824 |
os.unlink(video_path)
|