hotspotv1 / app.py
kavehtaheri's picture
Update app.py
21e5026 verified
import gradio as gr
import requests
import os
import json
import time
# CORRECTED IMPORT FOR RESTRICTED ENVIRONMENTS:
# We bypass the 'editor' module and import the class directly.
# This works now because requirements.txt ensures a full installation.
from moviepy.video.io.VideoFileClip import VideoFileClip
import traceback # Import traceback for detailed error logging
# --- 1. CONFIGURATION & CONSTANTS ---
# Securely load API key from Hugging Face Space secrets.
ONE_API_KEY = os.environ.get("ONE_API_KEY", "268976:66f4f58a2a905")
# The custom endpoint for the one-api.ir service.
ONE_API_URL = "https://api.one-api.ir/chatbot/v1/gpt4o/"
# --- MASTER PROMPTS ---
PROMPT_SHORTS_MODE = """
You are an expert producer of viral short-form content. Analyze the provided SRT transcript to find the single most impactful, hook-worthy segment for a video under 3 minutes (ideally 60-90 seconds).
PRIORITIES: Strong Hook, Single Clear Point, High Energy, Clear Payoff.
**Input SRT Content:**
{transcript_content}
**Instructions:**
Your output MUST be a single, valid JSON object and nothing else. Do not include any text, code blocks, or explanations before or after the JSON object.
{{
"clip_title_suggestion": "A catchy, clickbait-style title for this short clip.",
"reasoning": "Briefly explain why this segment is perfect for a short video, referencing the hook and payoff.",
"final_clip_start_seconds": <The precise start time in total seconds from the SRT>,
"final_clip_end_seconds": <The precise end time in total seconds from the SRT>
}}
"""
PROMPT_NARRATIVE_MODE = """
You are an expert video editor and storyteller. Analyze the provided transcript to find the most compelling narrative segment between 5 and 12 minutes long.
METHODOLOGY: Identify the peak moment, then find the corresponding setup and resolution to create a complete narrative arc.
**Input SRT Content:**
{transcript_content}
**Instructions:**
Your output MUST be a single, valid JSON object and nothing else. Do not include any text, code blocks, or explanations before or after the JSON object.
{{
"narrative_summary": "A one-sentence summary of the story told in the extracted clip.",
"reasoning": "Explain why this segment works as a standalone narrative, mentioning the peak moment and how the start/end points provide a full arc.",
"final_clip_start_seconds": <The precise start time in total seconds from the SRT>,
"final_clip_end_seconds": <The precise end time in total seconds from the SRT>
}}
"""
# --- 2. LLM AGENT WRAPPER ---
def call_gpt4o_oneapi(transcript_content, prompt_template):
"""Makes a robust, custom API call to the one-api.ir service."""
if not ONE_API_KEY or not ONE_API_URL:
raise ValueError("ONE_API_KEY and ONE_API_URL secrets are not set correctly.")
headers = {
"one-api-token": ONE_API_KEY,
"Content-Type": "application/json"
}
final_prompt = prompt_template.format(transcript_content=transcript_content)
payload = [{"role": "user", "content": final_prompt}]
try:
response = requests.post(ONE_API_URL, headers=headers, json=payload, timeout=180)
response.raise_for_status()
result = response.json()
if "result" not in result or not isinstance(result.get("result"), list) or len(result["result"]) == 0:
return f"Error: Unexpected JSON structure from API. 'result' list not found or empty.\nFull response: {json.dumps(result)}"
first_item_in_result = result["result"][0]
if isinstance(first_item_in_result, dict):
message_content = first_item_in_result.get("content")
if message_content:
return message_content
else:
return f"Error: 'content' key not found in API response dictionary.\nFull response: {json.dumps(result)}"
elif isinstance(first_item_in_result, str):
return first_item_in_result
else:
return f"Error: Unknown item type in API 'result' list.\nFull response: {json.dumps(result)}"
except requests.exceptions.HTTPError as e:
return f"HTTP Error calling API: {e}\nResponse Body: {e.response.text}"
except requests.exceptions.RequestException as e:
return f"Error connecting to API: {str(e)}"
except json.JSONDecodeError:
return f"Error: Failed to decode JSON from API response.\nResponse Body: {response.text}"
# --- 3. CORE ORCHESTRATOR FUNCTION ---
def generate_viral_clip(video_file, srt_file, analysis_mode, progress=gr.Progress()):
if not video_file or not srt_file:
return "Error: Please upload both a video file and an SRT file.", None
if not ONE_API_KEY or not ONE_API_URL:
return "Error: API keys for OneAPI are not configured correctly in the Space secrets.", None
video = None
new_clip = None
try:
progress(0.1, desc="Reading SRT file...")
with open(srt_file, 'r', encoding='utf-8') as f:
transcript_content = f.read()
progress(0.2, desc="Preparing analysis prompt...")
prompt_template = PROMPT_SHORTS_MODE if analysis_mode == "Viral Spot for Shorts (< 3 mins)" else PROMPT_NARRATIVE_MODE
progress(0.4, desc="Calling AI for analysis...")
llm_response_str = call_gpt4o_oneapi(transcript_content, prompt_template)
progress(0.7, desc="Parsing AI response...")
if llm_response_str.startswith("Error") or llm_response_str.startswith("HTTP Error"):
return llm_response_str, None
try:
cleaned_response = llm_response_str.strip()
if cleaned_response.startswith("```json"):
cleaned_response = cleaned_response[7:]
if cleaned_response.endswith("```"):
cleaned_response = cleaned_response[:-3]
parsed_response = json.loads(cleaned_response)
if not isinstance(parsed_response, dict):
raise TypeError(f"AI did not return a valid JSON object. It returned a {type(parsed_response).__name__}.")
start_time = float(parsed_response['final_clip_start_seconds'])
end_time = float(parsed_response['final_clip_end_seconds'])
reasoning = parsed_response.get('reasoning', 'No reasoning provided.')
summary = (f"โœ… Analysis Complete!\n\n"
f"Reasoning: {reasoning}\n\n"
f"Title Suggestion: {parsed_response.get('clip_title_suggestion', 'N/A')}\n"
f"Narrative Summary: {parsed_response.get('narrative_summary', 'N/A')}\n\n"
f"Clipping video from {time.strftime('%H:%M:%S', time.gmtime(start_time))} to {time.strftime('%H:%M:%S', time.gmtime(end_time))}.")
except (json.JSONDecodeError, KeyError, TypeError) as e:
error_msg = f"Error: Failed to parse AI response. Details: {e}\n\nRaw AI Response:\n---\n{llm_response_str}"
return error_msg, None
progress(0.8, desc="Clipping video...")
output_filename = "viral_clip.mp4"
video = VideoFileClip(video_file)
if end_time > video.duration:
end_time = video.duration
summary += f"\n\nโš ๏ธ Warning: End time was beyond video duration, adjusted to {end_time:.2f}s."
new_clip = video.subclipped(start_time, end_time)
new_clip.write_videofile(output_filename, codec="libx264", audio_codec="aac")
progress(1.0, desc="Done!")
return summary, output_filename
except Exception as e:
tb_str = traceback.format_exc()
return f"An unexpected error occurred in the main process: {str(e)}\n\nTraceback:\n{tb_str}", None
finally:
# Gracefully close the video clips to release file handles
if new_clip:
try:
new_clip.close()
except Exception:
pass # Ignore errors on close
if video:
try:
video.close()
except Exception:
pass # Ignore errors on close
# --- 4. GRADIO UI DEFINITION ---
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown(
"""
# ๐ŸŽฌ AI Viral Video Extractor
This tool uses an AI agent to analyze a video transcript and automatically clip the most viral segment.
**โš ๏ธ Important Setup:** For best security, configure `ONE_API_KEY` in your Hugging Face **Space Settings > Secrets**.
"""
)
with gr.Row():
with gr.Column(scale=1):
video_input = gr.Video(label="1. Upload Original Video")
srt_input = gr.File(label="2. Upload English SRT File", file_types=['.srt'])
mode_input = gr.Radio(
label="3. Select Analysis Mode",
choices=["Viral Spot for Shorts (< 3 mins)", "Viral Narrative Clip (5-12 mins)"],
value="Viral Narrative Clip (5-12 mins)"
)
submit_button = gr.Button("๐Ÿš€ Generate Viral Clip", variant="primary")
with gr.Column(scale=2):
summary_output = gr.Textbox(label="Analysis Summary", lines=12, interactive=False)
video_output = gr.Video(label="Generated Clip", interactive=False)
submit_button.click(
fn=generate_viral_clip,
inputs=[video_input, srt_input, mode_input],
outputs=[summary_output, video_output],
)
if __name__ == "__main__":
demo.launch(debug=True)