Spaces:
Sleeping
Sleeping
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) | |