rejig-ai commited on
Commit
0d64dc9
·
1 Parent(s): f3ba3f9

HeyGen video end-to-end integration - hard-coded video script

Browse files
agent_system/video_handler.py CHANGED
@@ -4,6 +4,7 @@ Handlers for video creation with stage-based status updates.
4
  import asyncio
5
  import logging
6
  from typing import Dict, Any, AsyncGenerator, List
 
7
  from .heygen_client import HeyGenClient
8
  from .video_status import VideoStatusTracker
9
  from config import HEYGEN_API_KEY, HEYGEN_API_BASE_URL, HEYGEN_AVATAR_ID, HEYGEN_VOICE_ID
@@ -13,6 +14,41 @@ logger = logging.getLogger(__name__)
13
  # Hardcoded test message - in a full implementation, this would come from the user
14
  TEST_VIDEO_MESSAGE = "Hey this is a test video. The HeyGen API integration is working correctly."
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  async def handle_video_with_stages(message: str, history: List[Dict[str, Any]]) -> AsyncGenerator[List[Dict[str, Any]], None]:
17
  """
18
  Handle video creation with stage-based status updates.
@@ -84,16 +120,26 @@ async def handle_video_with_stages(message: str, history: List[Dict[str, Any]])
84
  if status == "completed":
85
  # Video is ready
86
  completed = True
87
- video_url = (status_data.get("share_url") or
88
- status_data.get("video_url") or
89
- status_data.get("app_url") or
90
  "(URL not available)")
91
-
92
  logger.info(f"Video completed with URL: {video_url}")
93
-
94
- final_msg = f"Video created successfully! Watch it here: {video_url}"
 
 
 
 
 
 
95
  final_history = new_history.copy()
96
- final_history.append({"role": "assistant", "content": final_msg + "\n\n[Agent: Video Creator]"})
 
 
 
 
97
  yield final_history
98
 
99
  elif status in ["failed", "error"]:
 
4
  import asyncio
5
  import logging
6
  from typing import Dict, Any, AsyncGenerator, List
7
+ import bleach
8
  from .heygen_client import HeyGenClient
9
  from .video_status import VideoStatusTracker
10
  from config import HEYGEN_API_KEY, HEYGEN_API_BASE_URL, HEYGEN_AVATAR_ID, HEYGEN_VOICE_ID
 
14
  # Hardcoded test message - in a full implementation, this would come from the user
15
  TEST_VIDEO_MESSAGE = "Hey this is a test video. The HeyGen API integration is working correctly."
16
 
17
+ def sanitize_html(html_content):
18
+ """
19
+ Sanitize HTML content to prevent XSS attacks.
20
+ Only allow specific tags and attributes for video embedding.
21
+
22
+ Args:
23
+ html_content: Raw HTML content
24
+
25
+ Returns:
26
+ Sanitized HTML content
27
+ """
28
+ allowed_tags = ['iframe', 'div', 'a', 'br', 'span']
29
+ allowed_attributes = {
30
+ 'iframe': ['width', 'height', 'src', 'frameborder', 'allow', 'allowfullscreen', 'style'],
31
+ 'a': ['href', 'target', 'rel', 'class', 'style'],
32
+ 'div': ['class', 'style'],
33
+ 'span': ['class', 'style']
34
+ }
35
+
36
+ try:
37
+ # Try with older bleach versions that don't support styles parameter
38
+ return bleach.clean(
39
+ html_content,
40
+ tags=allowed_tags,
41
+ attributes=allowed_attributes,
42
+ protocols=['http', 'https']
43
+ )
44
+ except TypeError as e:
45
+ logger.warning(f"Bleach error: {e}, falling back to basic sanitization")
46
+ # For iframe content, we'll trust it since it's coming from known sources
47
+ if "<iframe" in html_content and "heygen.com" in html_content:
48
+ return html_content
49
+ # Otherwise do basic escaping
50
+ return html_content.replace("<", "&lt;").replace(">", "&gt;")
51
+
52
  async def handle_video_with_stages(message: str, history: List[Dict[str, Any]]) -> AsyncGenerator[List[Dict[str, Any]], None]:
53
  """
54
  Handle video creation with stage-based status updates.
 
120
  if status == "completed":
121
  # Video is ready
122
  completed = True
123
+ video_url = (status_data.get("share_url") or
124
+ status_data.get("video_url") or
125
+ status_data.get("app_url") or
126
  "(URL not available)")
127
+
128
  logger.info(f"Video completed with URL: {video_url}")
129
+
130
+ # Generate embedded video HTML and sanitize it
131
+ video_html = sanitize_html(tracker.get_embedded_video_html(video_url))
132
+
133
+ # Create final message with embedded video
134
+ final_msg = f"Video created successfully!"
135
+ final_content = f"{final_msg}\n\n{video_html}\n\n[Agent: Video Creator]"
136
+
137
  final_history = new_history.copy()
138
+ final_history.append({
139
+ "role": "assistant",
140
+ "content": final_content,
141
+ "html": True # Flag that this contains HTML
142
+ })
143
  yield final_history
144
 
145
  elif status in ["failed", "error"]:
agent_system/video_status.py CHANGED
@@ -3,6 +3,8 @@ Track and display video creation status with stage-based indicators.
3
  """
4
  import time
5
  import logging
 
 
6
 
7
  logger = logging.getLogger(__name__)
8
 
@@ -65,19 +67,102 @@ class VideoStatusTracker:
65
  def get_status_message(self):
66
  """
67
  Get a formatted status message with elapsed time.
68
-
69
  Returns:
70
  Formatted status message string
71
  """
72
  elapsed = int(time.time() - self.start_time)
73
  minutes = elapsed // 60
74
  seconds = elapsed % 60
75
-
76
  # Simple spinner characters (can be displayed in text)
77
  spinner = "-\\|/"[int(time.time() * 2) % 4]
78
-
79
  # Only show spinner for in-progress states
80
  if self.current_stage in ["queued", "processing", "rendering"]:
81
  return f"{spinner} {self.STAGES[self.current_stage]} ({minutes}m {seconds}s elapsed)"
82
  else:
83
- return f"{self.STAGES[self.current_stage]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  import time
5
  import logging
6
+ import re
7
+ from typing import Optional
8
 
9
  logger = logging.getLogger(__name__)
10
 
 
67
  def get_status_message(self):
68
  """
69
  Get a formatted status message with elapsed time.
70
+
71
  Returns:
72
  Formatted status message string
73
  """
74
  elapsed = int(time.time() - self.start_time)
75
  minutes = elapsed // 60
76
  seconds = elapsed % 60
77
+
78
  # Simple spinner characters (can be displayed in text)
79
  spinner = "-\\|/"[int(time.time() * 2) % 4]
80
+
81
  # Only show spinner for in-progress states
82
  if self.current_stage in ["queued", "processing", "rendering"]:
83
  return f"{spinner} {self.STAGES[self.current_stage]} ({minutes}m {seconds}s elapsed)"
84
  else:
85
+ return f"{self.STAGES[self.current_stage]}"
86
+
87
+ def get_embedded_video_html(self, video_url: str) -> str:
88
+ """
89
+ Generate HTML for embedding the video in chat interface.
90
+
91
+ Args:
92
+ video_url: URL of the completed video
93
+
94
+ Returns:
95
+ HTML string with embedded video player
96
+ """
97
+ # Extract the video ID from the URL
98
+ video_id = self._extract_video_id(video_url)
99
+
100
+ if video_id:
101
+ # Create iframe embed code for HeyGen videos
102
+ return f"""
103
+ <div style="margin: 10px 0; width: 100%; max-width: 640px;">
104
+ <iframe
105
+ width="100%"
106
+ height="315"
107
+ src="https://app.heygen.com/embed/{video_id}"
108
+ frameborder="0"
109
+ allow="autoplay; encrypted-media"
110
+ allowfullscreen>
111
+ </iframe>
112
+ <div style="margin-top: 5px; font-size: 0.9em;">
113
+ <a href="{video_url}" target="_blank" rel="noopener noreferrer">Open in new window</a>
114
+ </div>
115
+ </div>
116
+ """
117
+ else:
118
+ # Fallback to providing the direct link
119
+ return f'<div style="margin: 10px 0;"><a href="{video_url}" target="_blank" rel="noopener noreferrer">Watch video</a></div>'
120
+
121
+ def _extract_video_id(self, url: str) -> Optional[str]:
122
+ """
123
+ Extract the video ID from various HeyGen URL formats.
124
+
125
+ Args:
126
+ url: HeyGen video URL
127
+
128
+ Returns:
129
+ Video ID or None if not found
130
+ """
131
+ # Handle share URLs: https://app.heygen.com/share/{video_id}
132
+ share_match = re.search(r'app\.heygen\.com/share/([a-zA-Z0-9_-]+)', url)
133
+ if share_match:
134
+ return share_match.group(1)
135
+
136
+ # Handle app URLs: https://app.heygen.com/video/{video_id}
137
+ app_match = re.search(r'app\.heygen\.com/video/([a-zA-Z0-9_-]+)', url)
138
+ if app_match:
139
+ return app_match.group(1)
140
+
141
+ # Handle embed URLs: https://app.heygen.com/embed/{video_id}
142
+ embed_match = re.search(r'app\.heygen\.com/embed/([a-zA-Z0-9_-]+)', url)
143
+ if embed_match:
144
+ return embed_match.group(1)
145
+
146
+ # Handle videos URL: https://app.heygen.com/videos/{video_id}
147
+ videos_match = re.search(r'app\.heygen\.com/videos/([a-zA-Z0-9_-]+)', url)
148
+ if videos_match:
149
+ return videos_match.group(1)
150
+
151
+ # Handle direct video URL with heygen CDN - various formats
152
+ cdn_match = re.search(r'heygen\.ai/[^/]*/([a-zA-Z0-9/_-]+)\.mp4', url)
153
+ if cdn_match:
154
+ # For direct video URLs, we can't use the embed directly
155
+ # Log this and return None to fall back to a direct link
156
+ video_path = cdn_match.group(1)
157
+ logger.info(f"Found direct CDN URL, path: {video_path}")
158
+ # Don't try to create an embed for direct CDN URLs
159
+ return None
160
+
161
+ # Handle raw files2.heygen.ai URLs
162
+ if "files2.heygen.ai" in url or "aws_pacific" in url:
163
+ logger.info(f"Found direct AWS URL, using fallback link")
164
+ # Don't try to create an embed for S3/CDN direct URLs
165
+ return None
166
+
167
+ logger.warning(f"Could not extract video ID from URL: {url}")
168
+ return None
requirements.txt CHANGED
@@ -1,4 +1,6 @@
1
  openai>=1.0.0
2
  openai-agents
3
  gradio>=4.0
4
- python-dotenv
 
 
 
1
  openai>=1.0.0
2
  openai-agents
3
  gradio>=4.0
4
+ python-dotenv
5
+ aiohttp
6
+ bleach
ui/chat_interface.py CHANGED
@@ -73,6 +73,12 @@ async def chat_with_agents(message: str, history: List[Dict[str, Any]]):
73
 
74
  # Use the enhanced stage-based video handler
75
  async for updated_history in handle_video_with_stages(message, new_history):
 
 
 
 
 
 
76
  yield updated_history
77
  return
78
 
@@ -119,17 +125,30 @@ def create_chat_interface():
119
  background: #E6F3FF;
120
  color: #1E90FF;
121
  }
 
 
 
 
 
 
 
 
 
 
 
122
  """
123
-
124
  with gr.Blocks(css=custom_css, theme="soft") as demo:
125
  gr.HTML("<h1 style='text-align: center'>✨ AI Agent Chat ✨</h1>")
126
  gr.HTML("<p style='text-align: center'>Please login with your email to enjoy jokes and poems!</p>")
127
-
128
  chatbot = gr.Chatbot(
129
  height=500,
130
  type="messages",
131
  show_copy_button=True,
132
- bubble_full_width=False
 
 
133
  )
134
 
135
  with gr.Row():
 
73
 
74
  # Use the enhanced stage-based video handler
75
  async for updated_history in handle_video_with_stages(message, new_history):
76
+ # Process any HTML in messages
77
+ for msg in updated_history:
78
+ if msg.get("role") == "assistant" and msg.get("html"):
79
+ # Process HTML message but don't strip the HTML
80
+ if "html" in msg:
81
+ del msg["html"] # Remove the flag after processing
82
  yield updated_history
83
  return
84
 
 
125
  background: #E6F3FF;
126
  color: #1E90FF;
127
  }
128
+
129
+ .heygen-video-embed {
130
+ margin: 10px 0;
131
+ width: 100%;
132
+ max-width: 640px;
133
+ }
134
+
135
+ .video-link {
136
+ margin-top: 5px;
137
+ font-size: 0.9em;
138
+ }
139
  """
140
+
141
  with gr.Blocks(css=custom_css, theme="soft") as demo:
142
  gr.HTML("<h1 style='text-align: center'>✨ AI Agent Chat ✨</h1>")
143
  gr.HTML("<p style='text-align: center'>Please login with your email to enjoy jokes and poems!</p>")
144
+
145
  chatbot = gr.Chatbot(
146
  height=500,
147
  type="messages",
148
  show_copy_button=True,
149
+ bubble_full_width=False,
150
+ render_markdown=True, # Enable markdown rendering
151
+ sanitize_html=False # Allow HTML to be rendered
152
  )
153
 
154
  with gr.Row():