File size: 9,479 Bytes
c157612
b12b399
c157612
b12b399
c157612
c83ff8b
 
 
 
b12b399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32860f3
 
b12b399
 
 
 
 
 
32860f3
b12b399
32860f3
b12b399
 
 
 
32860f3
b12b399
 
 
32860f3
b12b399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32860f3
b12b399
 
 
 
 
 
21e5026
b12b399
efe809b
b12b399
 
2daed77
b12b399
 
 
32860f3
6daf130
c83ff8b
b12b399
c83ff8b
 
 
 
b12b399
c83ff8b
 
 
 
b12b399
 
 
 
 
 
 
 
 
 
c157612
b12b399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c157612
 
eaad797
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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)