akhaliq HF Staff commited on
Commit
750aff3
Β·
1 Parent(s): a11dbaa

add decart video to video

Browse files
Files changed (1) hide show
  1. app.py +408 -17
app.py CHANGED
@@ -2660,12 +2660,13 @@ def compress_video_for_data_uri(video_bytes: bytes, max_size_mb: int = 8) -> byt
2660
  temp_output_path = temp_input_path.replace('.mp4', '_compressed.mp4')
2661
 
2662
  try:
2663
- # Compress with ffmpeg - aggressive settings for small size
2664
  subprocess.run([
2665
  'ffmpeg', '-i', temp_input_path,
2666
- '-vcodec', 'libx264', '-crf', '30', '-preset', 'fast',
2667
- '-vf', 'scale=480:-1', '-r', '15', # Lower resolution and frame rate
2668
  '-an', # Remove audio to save space
 
2669
  '-y', temp_output_path
2670
  ], check=True, capture_output=True, stderr=subprocess.DEVNULL)
2671
 
@@ -3504,6 +3505,114 @@ def generate_video_from_text(prompt: str, session_id: Optional[str] = None, toke
3504
  print(f"Text-to-video generation error: {str(e)}")
3505
  return f"Error generating video (text-to-video): {str(e)}"
3506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3507
  def generate_music_from_text(prompt: str, music_length_ms: int = 30000, session_id: Optional[str] = None, token: gr.OAuthToken | None = None) -> str:
3508
  """Generate music from a text prompt using ElevenLabs Music API and return an HTML <audio> tag.
3509
 
@@ -4120,7 +4229,94 @@ def create_video_replacement_blocks_from_input_image(html_content: str, user_pro
4120
  print("[Image2Video] No <body> tag; appending video via replacement block")
4121
  return f"{SEARCH_START}\n\n{DIVIDER}\n{video_html}\n{REPLACE_END}"
4122
 
4123
- def apply_generated_media_to_html(html_content: str, user_prompt: str, enable_text_to_image: bool, enable_image_to_image: bool, input_image_data, image_to_image_prompt: str | None = None, text_to_image_prompt: str | None = None, enable_image_to_video: bool = False, image_to_video_prompt: str | None = None, session_id: Optional[str] = None, enable_text_to_video: bool = False, text_to_video_prompt: Optional[str] = None, enable_text_to_music: bool = False, text_to_music_prompt: Optional[str] = None, token: gr.OAuthToken | None = None) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4124
  """Apply text/image/video/music replacements to HTML content.
4125
 
4126
  - Works with single-document HTML strings
@@ -4150,7 +4346,7 @@ def apply_generated_media_to_html(html_content: str, user_prompt: str, enable_te
4150
  try:
4151
  print(
4152
  f"[MediaApply] enable_i2v={enable_image_to_video}, enable_i2i={enable_image_to_image}, "
4153
- f"enable_t2i={enable_text_to_image}, enable_t2v={enable_text_to_video}, enable_t2m={enable_text_to_music}, has_image={input_image_data is not None}"
4154
  )
4155
  # If image-to-video is enabled, replace the first image with a generated video and return.
4156
  if enable_image_to_video and input_image_data is not None and (result.strip().startswith('<!DOCTYPE html>') or result.strip().startswith('<html')):
@@ -4195,6 +4391,50 @@ def apply_generated_media_to_html(html_content: str, user_prompt: str, enable_te
4195
  return format_multipage_output(multipage_files)
4196
  return result
4197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4198
  # If text-to-video is enabled, insert a generated video (no input image required) and return.
4199
  if enable_text_to_video and (result.strip().startswith('<!DOCTYPE html>') or result.strip().startswith('<html')):
4200
  t2v_prompt = (text_to_video_prompt or user_prompt or "").strip()
@@ -4606,9 +4846,22 @@ def send_to_sandbox(code):
4606
  with open(path, 'rb') as _f:
4607
  raw = _f.read()
4608
  mime = _mtypes.guess_type(path)[0] or 'application/octet-stream'
 
 
 
 
 
 
 
 
 
 
 
 
4609
  b64 = _b64.b64encode(raw).decode()
4610
  return f"data:{mime};base64,{b64}"
4611
- except Exception:
 
4612
  return None
4613
  def _repl_double(m):
4614
  url = m.group(1)
@@ -4620,6 +4873,36 @@ def send_to_sandbox(code):
4620
  return f"src='{data_uri}'" if data_uri else m.group(0)
4621
  html_doc = re.sub(r'src="(file:[^"]+)"', _repl_double, html_doc)
4622
  html_doc = re.sub(r"src='(file:[^']+)'", _repl_single, html_doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4623
  except Exception:
4624
  # Best-effort; continue without inlining
4625
  pass
@@ -4648,9 +4931,22 @@ def send_to_sandbox_with_refresh(code):
4648
  with open(path, 'rb') as _f:
4649
  raw = _f.read()
4650
  mime = _mtypes.guess_type(path)[0] or 'application/octet-stream'
 
 
 
 
 
 
 
 
 
 
 
 
4651
  b64 = _b64.b64encode(raw).decode()
4652
  return f"data:{mime};base64,{b64}"
4653
- except Exception:
 
4654
  return None
4655
  def _repl_double(m):
4656
  url = m.group(1)
@@ -4662,6 +4958,36 @@ def send_to_sandbox_with_refresh(code):
4662
  return f"src='{data_uri}'" if data_uri else m.group(0)
4663
  html_doc = re.sub(r'src="(file:[^"]+)"', _repl_double, html_doc)
4664
  html_doc = re.sub(r"src='(file:[^']+)'", _repl_single, html_doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4665
  except Exception:
4666
  # Best-effort; continue without inlining
4667
  pass
@@ -5175,7 +5501,7 @@ The HTML code above contains the complete original website structure with all im
5175
  stop_generation = False
5176
 
5177
 
5178
- def generation_code(query: Optional[str], vlm_image: Optional[gr.Image], gen_image: Optional[gr.Image], file: Optional[str], website_url: Optional[str], _setting: Dict[str, str], _history: Optional[History], _current_model: Dict, enable_search: bool = False, language: str = "html", provider: str = "auto", enable_image_generation: bool = False, enable_image_to_image: bool = False, image_to_image_prompt: Optional[str] = None, text_to_image_prompt: Optional[str] = None, enable_image_to_video: bool = False, image_to_video_prompt: Optional[str] = None, enable_text_to_video: bool = False, text_to_video_prompt: Optional[str] = None, enable_text_to_music: bool = False, text_to_music_prompt: Optional[str] = None):
5179
  if query is None:
5180
  query = ''
5181
  if _history is None:
@@ -5447,6 +5773,9 @@ This will help me create a better design for you."""
5447
  session_id=session_id,
5448
  enable_text_to_video=enable_text_to_video,
5449
  text_to_video_prompt=text_to_video_prompt,
 
 
 
5450
  enable_text_to_music=enable_text_to_music,
5451
  text_to_music_prompt=text_to_music_prompt,
5452
  )
@@ -5518,6 +5847,9 @@ This will help me create a better design for you."""
5518
  session_id=session_id,
5519
  enable_text_to_video=enable_text_to_video,
5520
  text_to_video_prompt=text_to_video_prompt,
 
 
 
5521
  enable_text_to_music=enable_text_to_music,
5522
  text_to_music_prompt=text_to_music_prompt,
5523
  token=None,
@@ -5547,6 +5879,9 @@ This will help me create a better design for you."""
5547
  session_id=session_id,
5548
  enable_text_to_video=enable_text_to_video,
5549
  text_to_video_prompt=text_to_video_prompt,
 
 
 
5550
  enable_text_to_music=enable_text_to_music,
5551
  text_to_music_prompt=text_to_music_prompt,
5552
  token=None,
@@ -5985,6 +6320,9 @@ This will help me create a better design for you."""
5985
  text_to_image_prompt=text_to_image_prompt,
5986
  enable_text_to_video=enable_text_to_video,
5987
  text_to_video_prompt=text_to_video_prompt,
 
 
 
5988
  enable_text_to_music=enable_text_to_music,
5989
  text_to_music_prompt=text_to_music_prompt,
5990
  token=None,
@@ -6017,6 +6355,9 @@ This will help me create a better design for you."""
6017
  session_id=session_id,
6018
  enable_text_to_video=enable_text_to_video,
6019
  text_to_video_prompt=text_to_video_prompt,
 
 
 
6020
  enable_text_to_music=enable_text_to_music,
6021
  text_to_music_prompt=text_to_music_prompt,
6022
  )
@@ -7286,6 +7627,24 @@ with gr.Blocks(
7286
  visible=False
7287
  )
7288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7289
  # Text-to-Music
7290
  text_to_music_toggle = gr.Checkbox(
7291
  label="🎡 Generate Music (text β†’ music)",
@@ -7335,6 +7694,11 @@ with gr.Blocks(
7335
  inputs=[text_to_video_toggle, beta_toggle],
7336
  outputs=[text_to_video_prompt]
7337
  )
 
 
 
 
 
7338
  text_to_music_toggle.change(
7339
  on_text_to_image_toggle,
7340
  inputs=[text_to_music_toggle, beta_toggle],
@@ -7892,7 +8256,7 @@ with gr.Blocks(
7892
  show_progress="hidden",
7893
  ).then(
7894
  generation_code,
7895
- inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, text_to_music_toggle, text_to_music_prompt],
7896
  outputs=[code_output, history, sandbox, history_output]
7897
  ).then(
7898
  end_generation_ui,
@@ -7933,7 +8297,7 @@ with gr.Blocks(
7933
  show_progress="hidden",
7934
  ).then(
7935
  generation_code,
7936
- inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, text_to_music_toggle, text_to_music_prompt],
7937
  outputs=[code_output, history, sandbox, history_output]
7938
  ).then(
7939
  end_generation_ui,
@@ -7982,6 +8346,9 @@ with gr.Blocks(
7982
  upd_i2v_prompt = gr.skip()
7983
  upd_t2v_toggle = gr.skip()
7984
  upd_t2v_prompt = gr.skip()
 
 
 
7985
  upd_model_dropdown = gr.skip()
7986
  upd_current_model = gr.skip()
7987
  upd_t2m_toggle = gr.skip()
@@ -8051,6 +8418,13 @@ with gr.Blocks(
8051
  if p:
8052
  upd_t2v_prompt = gr.update(value=p)
8053
 
 
 
 
 
 
 
 
8054
  # Text-to-music
8055
  if ("text to music" in seg_norm) or ("text-to-music" in seg_norm) or ("generate music" in seg_norm) or ("compose music" in seg_norm):
8056
  upd_t2m_toggle = gr.update(value=True)
@@ -8075,9 +8449,10 @@ with gr.Blocks(
8075
  upd_model_dropdown = gr.update(value=model_obj["name"]) # keep dropdown in sync
8076
  upd_current_model = model_obj # pass directly to State for immediate effect
8077
 
8078
- # Files: attach first non-image to file_input; image to generation_image_input
8079
  img_assigned = False
8080
- non_img_assigned = False
 
8081
  for f in files:
8082
  try:
8083
  path = f["path"] if isinstance(f, dict) and "path" in f else f
@@ -8088,9 +8463,12 @@ with gr.Blocks(
8088
  if not img_assigned and any(str(path).lower().endswith(ext) for ext in [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".tiff", ".tif"]):
8089
  upd_image_for_gen = gr.update(value=path)
8090
  img_assigned = True
8091
- elif not non_img_assigned:
 
 
 
8092
  upd_file = gr.update(value=path)
8093
- non_img_assigned = True
8094
 
8095
  # Set main build intent from first segment (if present), otherwise full text
8096
  if main_prompt:
@@ -8120,6 +8498,9 @@ with gr.Blocks(
8120
  upd_i2v_prompt,
8121
  upd_t2v_toggle,
8122
  upd_t2v_prompt,
 
 
 
8123
  upd_model_dropdown,
8124
  upd_current_model,
8125
  upd_t2m_toggle,
@@ -8147,6 +8528,9 @@ with gr.Blocks(
8147
  image_to_video_prompt,
8148
  text_to_video_toggle,
8149
  text_to_video_prompt,
 
 
 
8150
  model_dropdown,
8151
  current_model,
8152
  text_to_music_toggle,
@@ -8160,7 +8544,7 @@ with gr.Blocks(
8160
  show_progress="hidden",
8161
  ).then(
8162
  generation_code,
8163
- inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, text_to_music_toggle, text_to_music_prompt],
8164
  outputs=[code_output, history, sandbox, history_output]
8165
  ).then(
8166
  end_generation_ui,
@@ -8192,12 +8576,13 @@ with gr.Blocks(
8192
  )
8193
 
8194
  # Toggle between classic controls and beta chat UI
8195
- def toggle_beta(checked: bool, t2i: bool, i2i: bool, i2v: bool, t2v: bool, t2m: bool):
8196
  # Prompts only visible in classic mode and when their toggles are on
8197
  t2i_vis = (not checked) and bool(t2i)
8198
  i2i_vis = (not checked) and bool(i2i)
8199
  i2v_vis = (not checked) and bool(i2v)
8200
  t2v_vis = (not checked) and bool(t2v)
 
8201
  t2m_vis = (not checked) and bool(t2m)
8202
 
8203
  return (
@@ -8222,6 +8607,9 @@ with gr.Blocks(
8222
  gr.update(visible=i2v_vis), # image_to_video_prompt
8223
  gr.update(visible=not checked), # text_to_video_toggle
8224
  gr.update(visible=t2v_vis), # text_to_video_prompt
 
 
 
8225
  gr.update(visible=not checked), # text_to_music_toggle
8226
  gr.update(visible=t2m_vis), # text_to_music_prompt
8227
  gr.update(visible=not checked), # model_dropdown
@@ -8231,7 +8619,7 @@ with gr.Blocks(
8231
 
8232
  beta_toggle.change(
8233
  toggle_beta,
8234
- inputs=[beta_toggle, image_generation_toggle, image_to_image_toggle, image_to_video_toggle, text_to_video_toggle, text_to_music_toggle],
8235
  outputs=[
8236
  sidebar_chatbot,
8237
  sidebar_msg,
@@ -8252,6 +8640,9 @@ with gr.Blocks(
8252
  image_to_video_prompt,
8253
  text_to_video_toggle,
8254
  text_to_video_prompt,
 
 
 
8255
  text_to_music_toggle,
8256
  text_to_music_prompt,
8257
  model_dropdown,
 
2660
  temp_output_path = temp_input_path.replace('.mp4', '_compressed.mp4')
2661
 
2662
  try:
2663
+ # Compress with ffmpeg - extremely aggressive settings for tiny preview size
2664
  subprocess.run([
2665
  'ffmpeg', '-i', temp_input_path,
2666
+ '-vcodec', 'libx264', '-crf', '40', '-preset', 'ultrafast',
2667
+ '-vf', 'scale=320:-1', '-r', '10', # Very low resolution and frame rate
2668
  '-an', # Remove audio to save space
2669
+ '-t', '10', # Limit to first 10 seconds for preview
2670
  '-y', temp_output_path
2671
  ], check=True, capture_output=True, stderr=subprocess.DEVNULL)
2672
 
 
3505
  print(f"Text-to-video generation error: {str(e)}")
3506
  return f"Error generating video (text-to-video): {str(e)}"
3507
 
3508
+ def generate_video_from_video(input_video_data, prompt: str, session_id: Optional[str] = None, token: gr.OAuthToken | None = None) -> str:
3509
+ """Generate a video from an input video and prompt using Decart AI's Lucy Pro V2V API.
3510
+
3511
+ Returns an HTML <video> tag whose source points to a temporary file URL.
3512
+ """
3513
+ try:
3514
+ print("[Video2Video] Starting video generation from video")
3515
+
3516
+ # Check for Decart API key
3517
+ api_key = os.getenv('DECART_API_KEY')
3518
+ if not api_key:
3519
+ print("[Video2Video] Missing DECART_API_KEY")
3520
+ return "Error: DECART_API_KEY environment variable is not set. Please set it to your Decart AI API token."
3521
+
3522
+ # Normalize input video to bytes
3523
+ import io
3524
+ import tempfile
3525
+
3526
+ def _load_video_bytes(video_like) -> bytes:
3527
+ if hasattr(video_like, 'read'):
3528
+ return video_like.read()
3529
+ if isinstance(video_like, (bytes, bytearray)):
3530
+ return bytes(video_like)
3531
+ if hasattr(video_like, 'name'): # File path
3532
+ with open(video_like.name, 'rb') as f:
3533
+ return f.read()
3534
+ # If it's a string, assume it's a file path
3535
+ if isinstance(video_like, str):
3536
+ with open(video_like, 'rb') as f:
3537
+ return f.read()
3538
+ return bytes(video_like)
3539
+
3540
+ video_bytes = _load_video_bytes(input_video_data)
3541
+ print(f"[Video2Video] Input video size: {len(video_bytes)} bytes")
3542
+
3543
+ # Prepare the API request
3544
+ form_data = {
3545
+ "prompt": prompt or "Enhance the video quality"
3546
+ }
3547
+
3548
+ # Create temporary file for video data
3549
+ with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as temp_file:
3550
+ temp_file.write(video_bytes)
3551
+ temp_file_path = temp_file.name
3552
+
3553
+ try:
3554
+ # Make API request to Decart AI
3555
+ with open(temp_file_path, "rb") as video_file:
3556
+ files = {"data": video_file}
3557
+ headers = {"X-API-KEY": api_key}
3558
+
3559
+ print(f"[Video2Video] Calling Decart API with prompt: {prompt}")
3560
+ response = requests.post(
3561
+ "https://api.decart.ai/v1/generate/lucy-pro-v2v",
3562
+ headers=headers,
3563
+ data=form_data,
3564
+ files=files,
3565
+ timeout=300 # 5 minute timeout
3566
+ )
3567
+
3568
+ if response.status_code != 200:
3569
+ print(f"[Video2Video] API request failed with status {response.status_code}: {response.text}")
3570
+ return f"Error: Decart API request failed with status {response.status_code}"
3571
+
3572
+ result_video_bytes = response.content
3573
+ print(f"[Video2Video] Received video bytes: {len(result_video_bytes)}")
3574
+
3575
+ finally:
3576
+ # Clean up temporary file
3577
+ try:
3578
+ os.unlink(temp_file_path)
3579
+ except Exception:
3580
+ pass
3581
+
3582
+ # Create temporary URL for preview (will be uploaded to HF during deploy)
3583
+ filename = "video_to_video_result.mp4"
3584
+ temp_url = upload_media_to_hf(result_video_bytes, filename, "video", token, use_temp=True)
3585
+
3586
+ # Check if creation was successful
3587
+ if temp_url.startswith("Error"):
3588
+ return temp_url
3589
+
3590
+ video_html = (
3591
+ f'<video controls autoplay muted loop playsinline '
3592
+ f'style="max-width: 100%; height: auto; border-radius: 8px; margin: 10px 0; display: block;" '
3593
+ f'onloadstart="this.style.backgroundColor=\'#f0f0f0\'" '
3594
+ f'onerror="this.style.display=\'none\'; console.error(\'Video failed to load\')">'
3595
+ f'<source src="{temp_url}" type="video/mp4" />'
3596
+ f'<p style="text-align: center; color: #666;">Your browser does not support the video tag.</p>'
3597
+ f'</video>'
3598
+ )
3599
+
3600
+ print(f"[Video2Video] Successfully generated video HTML tag with temporary URL: {temp_url}")
3601
+
3602
+ # Validate the generated video HTML
3603
+ if not validate_video_html(video_html):
3604
+ print("[Video2Video] Generated video HTML failed validation")
3605
+ return "Error: Generated video HTML is malformed"
3606
+
3607
+ return video_html
3608
+
3609
+ except Exception as e:
3610
+ import traceback
3611
+ print("[Video2Video] Exception during generation:")
3612
+ traceback.print_exc()
3613
+ print(f"Video-to-video generation error: {str(e)}")
3614
+ return f"Error generating video (video-to-video): {str(e)}"
3615
+
3616
  def generate_music_from_text(prompt: str, music_length_ms: int = 30000, session_id: Optional[str] = None, token: gr.OAuthToken | None = None) -> str:
3617
  """Generate music from a text prompt using ElevenLabs Music API and return an HTML <audio> tag.
3618
 
 
4229
  print("[Image2Video] No <body> tag; appending video via replacement block")
4230
  return f"{SEARCH_START}\n\n{DIVIDER}\n{video_html}\n{REPLACE_END}"
4231
 
4232
+ def create_video_replacement_blocks_from_input_video(html_content: str, user_prompt: str, input_video_data, session_id: Optional[str] = None) -> str:
4233
+ """Create search/replace blocks that replace the first <video> (or placeholder) with a generated <video>.
4234
+
4235
+ Uses generate_video_from_video to produce a single video and swaps it in.
4236
+ """
4237
+ if not user_prompt:
4238
+ return ""
4239
+
4240
+ import re
4241
+ print("[Video2Video] Creating replacement blocks for video replacement")
4242
+
4243
+ # Look for existing video elements first
4244
+ video_patterns = [
4245
+ r'<video[^>]*>.*?</video>',
4246
+ r'<video[^>]*/>',
4247
+ r'<video[^>]*></video>',
4248
+ ]
4249
+
4250
+ placeholder_videos = []
4251
+ for pattern in video_patterns:
4252
+ matches = re.findall(pattern, html_content, re.IGNORECASE | re.DOTALL)
4253
+ if matches:
4254
+ placeholder_videos.extend(matches)
4255
+
4256
+ # If no videos found, look for video placeholders or divs that might represent videos
4257
+ if not placeholder_videos:
4258
+ placeholder_patterns = [
4259
+ r'<div[^>]*class=["\'][^"\']*video[^"\']*["\'][^>]*>.*?</div>',
4260
+ r'<div[^>]*id=["\'][^"\']*video[^"\']*["\'][^>]*>.*?</div>',
4261
+ r'<iframe[^>]*src=["\'][^"\']*youtube[^"\']*["\'][^>]*>.*?</iframe>',
4262
+ r'<iframe[^>]*src=["\'][^"\']*vimeo[^"\']*["\'][^>]*>.*?</iframe>',
4263
+ ]
4264
+ for pattern in placeholder_patterns:
4265
+ matches = re.findall(pattern, html_content, re.IGNORECASE | re.DOTALL)
4266
+ if matches:
4267
+ placeholder_videos.extend(matches)
4268
+
4269
+ print(f"[Video2Video] Found {len(placeholder_videos)} candidate video elements")
4270
+
4271
+ video_html = generate_video_from_video(input_video_data, user_prompt, session_id=session_id, token=None)
4272
+ try:
4273
+ has_file_src = 'src="' in video_html and video_html.count('src="') >= 1 and 'data:video/mp4;base64' not in video_html.split('src="', 1)[1]
4274
+ print(f"[Video2Video] Generated video HTML length={len(video_html)}; has_file_src={has_file_src}")
4275
+ except Exception:
4276
+ pass
4277
+ if video_html.startswith("Error"):
4278
+ print("[Video2Video] Video generation returned error; aborting replacement")
4279
+ return ""
4280
+
4281
+ if placeholder_videos:
4282
+ placeholder = placeholder_videos[0]
4283
+ placeholder_clean = re.sub(r'\s+', ' ', placeholder.strip())
4284
+ print("[Video2Video] Replacing first video placeholder with generated video")
4285
+ placeholder_variations = [
4286
+ # Try the exact string first to maximize replacement success
4287
+ placeholder,
4288
+ placeholder_clean,
4289
+ placeholder_clean.replace('"', "'"),
4290
+ placeholder_clean.replace("'", '"'),
4291
+ re.sub(r'\s+', ' ', placeholder_clean),
4292
+ placeholder_clean.replace(' ', ' '),
4293
+ ]
4294
+ blocks = []
4295
+ for variation in placeholder_variations:
4296
+ blocks.append(f"""{SEARCH_START}
4297
+ {variation}
4298
+ {DIVIDER}
4299
+ {video_html}
4300
+ {REPLACE_END}""")
4301
+ return '\n\n'.join(blocks)
4302
+
4303
+ if '<body' in html_content:
4304
+ body_start = html_content.find('<body')
4305
+ body_end = html_content.find('>', body_start) + 1
4306
+ opening_body_tag = html_content[body_start:body_end]
4307
+ print("[Video2Video] No <video> found; inserting video right after the opening <body> tag")
4308
+ print(f"[Video2Video] Opening <body> tag snippet: {opening_body_tag[:120]}")
4309
+ return f"""{SEARCH_START}
4310
+ {opening_body_tag}
4311
+ {DIVIDER}
4312
+ {opening_body_tag}
4313
+ {video_html}
4314
+ {REPLACE_END}"""
4315
+
4316
+ print("[Video2Video] No <body> tag; appending video via replacement block")
4317
+ return f"{SEARCH_START}\n\n{DIVIDER}\n{video_html}\n{REPLACE_END}"
4318
+
4319
+ def apply_generated_media_to_html(html_content: str, user_prompt: str, enable_text_to_image: bool, enable_image_to_image: bool, input_image_data, image_to_image_prompt: str | None = None, text_to_image_prompt: str | None = None, enable_image_to_video: bool = False, image_to_video_prompt: str | None = None, session_id: Optional[str] = None, enable_text_to_video: bool = False, text_to_video_prompt: Optional[str] = None, enable_video_to_video: bool = False, video_to_video_prompt: Optional[str] = None, input_video_data = None, enable_text_to_music: bool = False, text_to_music_prompt: Optional[str] = None, token: gr.OAuthToken | None = None) -> str:
4320
  """Apply text/image/video/music replacements to HTML content.
4321
 
4322
  - Works with single-document HTML strings
 
4346
  try:
4347
  print(
4348
  f"[MediaApply] enable_i2v={enable_image_to_video}, enable_i2i={enable_image_to_image}, "
4349
+ f"enable_t2i={enable_text_to_image}, enable_t2v={enable_text_to_video}, enable_v2v={enable_video_to_video}, enable_t2m={enable_text_to_music}, has_image={input_image_data is not None}, has_video={input_video_data is not None}"
4350
  )
4351
  # If image-to-video is enabled, replace the first image with a generated video and return.
4352
  if enable_image_to_video and input_image_data is not None and (result.strip().startswith('<!DOCTYPE html>') or result.strip().startswith('<html')):
 
4391
  return format_multipage_output(multipage_files)
4392
  return result
4393
 
4394
+ # If video-to-video is enabled, replace the first video with a generated video and return.
4395
+ if enable_video_to_video and input_video_data is not None and (result.strip().startswith('<!DOCTYPE html>') or result.strip().startswith('<html')):
4396
+ v2v_prompt = (video_to_video_prompt or user_prompt or "").strip()
4397
+ print(f"[MediaApply] Running video-to-video with prompt len={len(v2v_prompt)}")
4398
+ try:
4399
+ video_html_tag = generate_video_from_video(input_video_data, v2v_prompt, session_id=session_id, token=token)
4400
+ if not (video_html_tag or "").startswith("Error"):
4401
+ # Validate video HTML before attempting placement
4402
+ if validate_video_html(video_html_tag):
4403
+ blocks_v = llm_place_media(result, video_html_tag, media_kind="video")
4404
+ else:
4405
+ print("[MediaApply] Generated video HTML failed validation, skipping LLM placement")
4406
+ blocks_v = ""
4407
+ else:
4408
+ print(f"[MediaApply] Video generation failed: {video_html_tag}")
4409
+ blocks_v = ""
4410
+ except Exception as e:
4411
+ print(f"[MediaApply] Exception during video-to-video generation: {str(e)}")
4412
+ blocks_v = ""
4413
+ if not blocks_v:
4414
+ # Create fallback video replacement blocks
4415
+ blocks_v = create_video_replacement_blocks_from_input_video(result, v2v_prompt, input_video_data, session_id=session_id)
4416
+ if blocks_v:
4417
+ print("[MediaApply] Applying video-to-video replacement blocks")
4418
+ before_len = len(result)
4419
+ result_after = apply_search_replace_changes(result, blocks_v)
4420
+ after_len = len(result_after)
4421
+ changed = (result_after != result)
4422
+ print(f"[MediaApply] v2v blocks length={len(blocks_v)}; html before={before_len}, after={after_len}, changed={changed}")
4423
+ if not changed:
4424
+ print("[MediaApply] DEBUG: Replacement did not change content. Dumping first block:")
4425
+ try:
4426
+ first_block = blocks_v.split(REPLACE_END)[0][:1000]
4427
+ print(first_block)
4428
+ except Exception:
4429
+ pass
4430
+ result = result_after
4431
+ else:
4432
+ print("[MediaApply] No v2v replacement blocks generated")
4433
+ if is_multipage and entry_html_path:
4434
+ multipage_files[entry_html_path] = result
4435
+ return format_multipage_output(multipage_files)
4436
+ return result
4437
+
4438
  # If text-to-video is enabled, insert a generated video (no input image required) and return.
4439
  if enable_text_to_video and (result.strip().startswith('<!DOCTYPE html>') or result.strip().startswith('<html')):
4440
  t2v_prompt = (text_to_video_prompt or user_prompt or "").strip()
 
4846
  with open(path, 'rb') as _f:
4847
  raw = _f.read()
4848
  mime = _mtypes.guess_type(path)[0] or 'application/octet-stream'
4849
+
4850
+ # Compress video files before converting to data URI to prevent preview breaks
4851
+ if mime and mime.startswith('video/'):
4852
+ print(f"[Sandbox] Compressing video for preview: {len(raw)} bytes")
4853
+ raw = compress_video_for_data_uri(raw, max_size_mb=1) # Very small limit for preview
4854
+ print(f"[Sandbox] Compressed video size: {len(raw)} bytes")
4855
+
4856
+ # If still too large, skip video embedding for preview
4857
+ if len(raw) > 512 * 1024: # 512KB final limit
4858
+ print(f"[Sandbox] Video still too large after compression, using placeholder")
4859
+ return None # Let the replacement function handle the fallback
4860
+
4861
  b64 = _b64.b64encode(raw).decode()
4862
  return f"data:{mime};base64,{b64}"
4863
+ except Exception as e:
4864
+ print(f"[Sandbox] Failed to convert file URL to data URI: {str(e)}")
4865
  return None
4866
  def _repl_double(m):
4867
  url = m.group(1)
 
4873
  return f"src='{data_uri}'" if data_uri else m.group(0)
4874
  html_doc = re.sub(r'src="(file:[^"]+)"', _repl_double, html_doc)
4875
  html_doc = re.sub(r"src='(file:[^']+)'", _repl_single, html_doc)
4876
+
4877
+ # Add deployment message for videos that couldn't be converted
4878
+ if 'file://' in html_doc and ('video' in html_doc.lower() or '.mp4' in html_doc.lower()):
4879
+ deployment_notice = '''
4880
+ <div style="
4881
+ position: fixed;
4882
+ top: 10px;
4883
+ right: 10px;
4884
+ background: #ff6b35;
4885
+ color: white;
4886
+ padding: 12px 16px;
4887
+ border-radius: 8px;
4888
+ font-family: Arial, sans-serif;
4889
+ font-size: 14px;
4890
+ font-weight: bold;
4891
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
4892
+ z-index: 9999;
4893
+ max-width: 300px;
4894
+ text-align: center;
4895
+ ">
4896
+ πŸš€ Deploy app to see videos with permanent URLs!
4897
+ </div>
4898
+ '''
4899
+ # Insert the notice right after the opening body tag
4900
+ if '<body' in html_doc:
4901
+ body_end = html_doc.find('>', html_doc.find('<body')) + 1
4902
+ html_doc = html_doc[:body_end] + deployment_notice + html_doc[body_end:]
4903
+ else:
4904
+ html_doc = deployment_notice + html_doc
4905
+
4906
  except Exception:
4907
  # Best-effort; continue without inlining
4908
  pass
 
4931
  with open(path, 'rb') as _f:
4932
  raw = _f.read()
4933
  mime = _mtypes.guess_type(path)[0] or 'application/octet-stream'
4934
+
4935
+ # Compress video files before converting to data URI to prevent preview breaks
4936
+ if mime and mime.startswith('video/'):
4937
+ print(f"[Sandbox] Compressing video for preview: {len(raw)} bytes")
4938
+ raw = compress_video_for_data_uri(raw, max_size_mb=1) # Very small limit for preview
4939
+ print(f"[Sandbox] Compressed video size: {len(raw)} bytes")
4940
+
4941
+ # If still too large, skip video embedding for preview
4942
+ if len(raw) > 512 * 1024: # 512KB final limit
4943
+ print(f"[Sandbox] Video still too large after compression, using placeholder")
4944
+ return None # Let the replacement function handle the fallback
4945
+
4946
  b64 = _b64.b64encode(raw).decode()
4947
  return f"data:{mime};base64,{b64}"
4948
+ except Exception as e:
4949
+ print(f"[Sandbox] Failed to convert file URL to data URI: {str(e)}")
4950
  return None
4951
  def _repl_double(m):
4952
  url = m.group(1)
 
4958
  return f"src='{data_uri}'" if data_uri else m.group(0)
4959
  html_doc = re.sub(r'src="(file:[^"]+)"', _repl_double, html_doc)
4960
  html_doc = re.sub(r"src='(file:[^']+)'", _repl_single, html_doc)
4961
+
4962
+ # Add deployment message for videos that couldn't be converted
4963
+ if 'file://' in html_doc and ('video' in html_doc.lower() or '.mp4' in html_doc.lower()):
4964
+ deployment_notice = '''
4965
+ <div style="
4966
+ position: fixed;
4967
+ top: 10px;
4968
+ right: 10px;
4969
+ background: #ff6b35;
4970
+ color: white;
4971
+ padding: 12px 16px;
4972
+ border-radius: 8px;
4973
+ font-family: Arial, sans-serif;
4974
+ font-size: 14px;
4975
+ font-weight: bold;
4976
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
4977
+ z-index: 9999;
4978
+ max-width: 300px;
4979
+ text-align: center;
4980
+ ">
4981
+ πŸš€ Deploy app to see videos with permanent URLs!
4982
+ </div>
4983
+ '''
4984
+ # Insert the notice right after the opening body tag
4985
+ if '<body' in html_doc:
4986
+ body_end = html_doc.find('>', html_doc.find('<body')) + 1
4987
+ html_doc = html_doc[:body_end] + deployment_notice + html_doc[body_end:]
4988
+ else:
4989
+ html_doc = deployment_notice + html_doc
4990
+
4991
  except Exception:
4992
  # Best-effort; continue without inlining
4993
  pass
 
5501
  stop_generation = False
5502
 
5503
 
5504
+ def generation_code(query: Optional[str], vlm_image: Optional[gr.Image], gen_image: Optional[gr.Image], file: Optional[str], website_url: Optional[str], _setting: Dict[str, str], _history: Optional[History], _current_model: Dict, enable_search: bool = False, language: str = "html", provider: str = "auto", enable_image_generation: bool = False, enable_image_to_image: bool = False, image_to_image_prompt: Optional[str] = None, text_to_image_prompt: Optional[str] = None, enable_image_to_video: bool = False, image_to_video_prompt: Optional[str] = None, enable_text_to_video: bool = False, text_to_video_prompt: Optional[str] = None, enable_video_to_video: bool = False, video_to_video_prompt: Optional[str] = None, input_video_data = None, enable_text_to_music: bool = False, text_to_music_prompt: Optional[str] = None):
5505
  if query is None:
5506
  query = ''
5507
  if _history is None:
 
5773
  session_id=session_id,
5774
  enable_text_to_video=enable_text_to_video,
5775
  text_to_video_prompt=text_to_video_prompt,
5776
+ enable_video_to_video=enable_video_to_video,
5777
+ video_to_video_prompt=video_to_video_prompt,
5778
+ input_video_data=input_video_data,
5779
  enable_text_to_music=enable_text_to_music,
5780
  text_to_music_prompt=text_to_music_prompt,
5781
  )
 
5847
  session_id=session_id,
5848
  enable_text_to_video=enable_text_to_video,
5849
  text_to_video_prompt=text_to_video_prompt,
5850
+ enable_video_to_video=enable_video_to_video,
5851
+ video_to_video_prompt=video_to_video_prompt,
5852
+ input_video_data=input_video_data,
5853
  enable_text_to_music=enable_text_to_music,
5854
  text_to_music_prompt=text_to_music_prompt,
5855
  token=None,
 
5879
  session_id=session_id,
5880
  enable_text_to_video=enable_text_to_video,
5881
  text_to_video_prompt=text_to_video_prompt,
5882
+ enable_video_to_video=enable_video_to_video,
5883
+ video_to_video_prompt=video_to_video_prompt,
5884
+ input_video_data=input_video_data,
5885
  enable_text_to_music=enable_text_to_music,
5886
  text_to_music_prompt=text_to_music_prompt,
5887
  token=None,
 
6320
  text_to_image_prompt=text_to_image_prompt,
6321
  enable_text_to_video=enable_text_to_video,
6322
  text_to_video_prompt=text_to_video_prompt,
6323
+ enable_video_to_video=enable_video_to_video,
6324
+ video_to_video_prompt=video_to_video_prompt,
6325
+ input_video_data=input_video_data,
6326
  enable_text_to_music=enable_text_to_music,
6327
  text_to_music_prompt=text_to_music_prompt,
6328
  token=None,
 
6355
  session_id=session_id,
6356
  enable_text_to_video=enable_text_to_video,
6357
  text_to_video_prompt=text_to_video_prompt,
6358
+ enable_video_to_video=enable_video_to_video,
6359
+ video_to_video_prompt=video_to_video_prompt,
6360
+ input_video_data=input_video_data,
6361
  enable_text_to_music=enable_text_to_music,
6362
  text_to_music_prompt=text_to_music_prompt,
6363
  )
 
7627
  visible=False
7628
  )
7629
 
7630
+ # Video-to-Video
7631
+ video_to_video_toggle = gr.Checkbox(
7632
+ label="🎬 Video to Video (uses input video)",
7633
+ value=False,
7634
+ visible=True,
7635
+ info="Transform your uploaded video using Decart AI's Lucy Pro V2V"
7636
+ )
7637
+ video_to_video_prompt = gr.Textbox(
7638
+ label="Video-to-Video Prompt",
7639
+ placeholder="Describe the transformation (e.g., 'Change their shirt to black and shiny leather')",
7640
+ lines=2,
7641
+ visible=False
7642
+ )
7643
+ video_input = gr.Video(
7644
+ label="Input video for transformation",
7645
+ visible=False
7646
+ )
7647
+
7648
  # Text-to-Music
7649
  text_to_music_toggle = gr.Checkbox(
7650
  label="🎡 Generate Music (text β†’ music)",
 
7694
  inputs=[text_to_video_toggle, beta_toggle],
7695
  outputs=[text_to_video_prompt]
7696
  )
7697
+ video_to_video_toggle.change(
7698
+ on_image_to_video_toggle,
7699
+ inputs=[video_to_video_toggle, beta_toggle],
7700
+ outputs=[video_input, video_to_video_prompt]
7701
+ )
7702
  text_to_music_toggle.change(
7703
  on_text_to_image_toggle,
7704
  inputs=[text_to_music_toggle, beta_toggle],
 
8256
  show_progress="hidden",
8257
  ).then(
8258
  generation_code,
8259
+ inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, video_to_video_toggle, video_to_video_prompt, video_input, text_to_music_toggle, text_to_music_prompt],
8260
  outputs=[code_output, history, sandbox, history_output]
8261
  ).then(
8262
  end_generation_ui,
 
8297
  show_progress="hidden",
8298
  ).then(
8299
  generation_code,
8300
+ inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, video_to_video_toggle, video_to_video_prompt, video_input, text_to_music_toggle, text_to_music_prompt],
8301
  outputs=[code_output, history, sandbox, history_output]
8302
  ).then(
8303
  end_generation_ui,
 
8346
  upd_i2v_prompt = gr.skip()
8347
  upd_t2v_toggle = gr.skip()
8348
  upd_t2v_prompt = gr.skip()
8349
+ upd_v2v_toggle = gr.skip()
8350
+ upd_v2v_prompt = gr.skip()
8351
+ upd_video_input = gr.skip()
8352
  upd_model_dropdown = gr.skip()
8353
  upd_current_model = gr.skip()
8354
  upd_t2m_toggle = gr.skip()
 
8418
  if p:
8419
  upd_t2v_prompt = gr.update(value=p)
8420
 
8421
+ # Video-to-video
8422
+ if ("video to video" in seg_norm) or ("video-to-video" in seg_norm) or ("transform video" in seg_norm):
8423
+ upd_v2v_toggle = gr.update(value=True)
8424
+ p = after_colon(seg)
8425
+ if p:
8426
+ upd_v2v_prompt = gr.update(value=p)
8427
+
8428
  # Text-to-music
8429
  if ("text to music" in seg_norm) or ("text-to-music" in seg_norm) or ("generate music" in seg_norm) or ("compose music" in seg_norm):
8430
  upd_t2m_toggle = gr.update(value=True)
 
8449
  upd_model_dropdown = gr.update(value=model_obj["name"]) # keep dropdown in sync
8450
  upd_current_model = model_obj # pass directly to State for immediate effect
8451
 
8452
+ # Files: attach first non-image/video to file_input; image to generation_image_input; video to video_input
8453
  img_assigned = False
8454
+ video_assigned = False
8455
+ non_media_assigned = False
8456
  for f in files:
8457
  try:
8458
  path = f["path"] if isinstance(f, dict) and "path" in f else f
 
8463
  if not img_assigned and any(str(path).lower().endswith(ext) for ext in [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".tiff", ".tif"]):
8464
  upd_image_for_gen = gr.update(value=path)
8465
  img_assigned = True
8466
+ elif not video_assigned and any(str(path).lower().endswith(ext) for ext in [".mp4", ".avi", ".mov", ".mkv", ".webm", ".m4v"]):
8467
+ upd_video_input = gr.update(value=path)
8468
+ video_assigned = True
8469
+ elif not non_media_assigned:
8470
  upd_file = gr.update(value=path)
8471
+ non_media_assigned = True
8472
 
8473
  # Set main build intent from first segment (if present), otherwise full text
8474
  if main_prompt:
 
8498
  upd_i2v_prompt,
8499
  upd_t2v_toggle,
8500
  upd_t2v_prompt,
8501
+ upd_v2v_toggle,
8502
+ upd_v2v_prompt,
8503
+ upd_video_input,
8504
  upd_model_dropdown,
8505
  upd_current_model,
8506
  upd_t2m_toggle,
 
8528
  image_to_video_prompt,
8529
  text_to_video_toggle,
8530
  text_to_video_prompt,
8531
+ video_to_video_toggle,
8532
+ video_to_video_prompt,
8533
+ video_input,
8534
  model_dropdown,
8535
  current_model,
8536
  text_to_music_toggle,
 
8544
  show_progress="hidden",
8545
  ).then(
8546
  generation_code,
8547
+ inputs=[input, image_input, generation_image_input, file_input, website_url_input, setting, history, current_model, search_toggle, language_dropdown, provider_state, image_generation_toggle, image_to_image_toggle, image_to_image_prompt, text_to_image_prompt, image_to_video_toggle, image_to_video_prompt, text_to_video_toggle, text_to_video_prompt, video_to_video_toggle, video_to_video_prompt, video_input, text_to_music_toggle, text_to_music_prompt],
8548
  outputs=[code_output, history, sandbox, history_output]
8549
  ).then(
8550
  end_generation_ui,
 
8576
  )
8577
 
8578
  # Toggle between classic controls and beta chat UI
8579
+ def toggle_beta(checked: bool, t2i: bool, i2i: bool, i2v: bool, t2v: bool, v2v: bool, t2m: bool):
8580
  # Prompts only visible in classic mode and when their toggles are on
8581
  t2i_vis = (not checked) and bool(t2i)
8582
  i2i_vis = (not checked) and bool(i2i)
8583
  i2v_vis = (not checked) and bool(i2v)
8584
  t2v_vis = (not checked) and bool(t2v)
8585
+ v2v_vis = (not checked) and bool(v2v)
8586
  t2m_vis = (not checked) and bool(t2m)
8587
 
8588
  return (
 
8607
  gr.update(visible=i2v_vis), # image_to_video_prompt
8608
  gr.update(visible=not checked), # text_to_video_toggle
8609
  gr.update(visible=t2v_vis), # text_to_video_prompt
8610
+ gr.update(visible=not checked), # video_to_video_toggle
8611
+ gr.update(visible=v2v_vis), # video_to_video_prompt
8612
+ gr.update(visible=v2v_vis), # video_input
8613
  gr.update(visible=not checked), # text_to_music_toggle
8614
  gr.update(visible=t2m_vis), # text_to_music_prompt
8615
  gr.update(visible=not checked), # model_dropdown
 
8619
 
8620
  beta_toggle.change(
8621
  toggle_beta,
8622
+ inputs=[beta_toggle, image_generation_toggle, image_to_image_toggle, image_to_video_toggle, text_to_video_toggle, video_to_video_toggle, text_to_music_toggle],
8623
  outputs=[
8624
  sidebar_chatbot,
8625
  sidebar_msg,
 
8640
  image_to_video_prompt,
8641
  text_to_video_toggle,
8642
  text_to_video_prompt,
8643
+ video_to_video_toggle,
8644
+ video_to_video_prompt,
8645
+ video_input,
8646
  text_to_music_toggle,
8647
  text_to_music_prompt,
8648
  model_dropdown,