File size: 6,620 Bytes
cfe45d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import json
import dspy

from app.dspy_modules import ensure_dspy_configured


class EditScript(dspy.Signature):
    """
    Edit a video script based on user instructions. You are a helpful video editing
    assistant that modifies explainer video scripts.

    Given the current script (as JSON), user's edit request, and conversation history,
    produce ONLY the scenes that need to change. Return the updated scene(s) as a
    JSON array. Each scene MUST include an "order" field (1-based) so the system
    knows which scene to update.

    CRITICAL RULES:
    - Only return scenes that you actually changed.
    - Do NOT return unchanged scenes.
    - Preserve the "order" field exactly to match the original scene.
    - Never add or remove scenes -- only modify existing ones.
    - Do NOT touch Scene 1 (the title/intro scene) unless explicitly asked.
    """

    current_script: str = dspy.InputField(
        desc="Current video script as JSON array of scenes (each has 'order', 'title', 'narration', 'visual_description', 'duration_seconds')"
    )
    user_request: str = dspy.InputField(
        desc="The user's edit request in natural language"
    )
    conversation_history: str = dspy.InputField(
        desc="Previous chat messages for context"
    )

    updated_scenes: str = dspy.OutputField(
        desc='JSON array of ONLY the modified scenes. Each MUST include "order" (int), '
        '"title" (str), "narration" (str), "visual_description" (str), "duration_seconds" (int). '
        'Return ONLY changed scenes, not the entire script.'
    )
    changes_made: str = dspy.OutputField(
        desc="Brief summary of what changes were made"
    )
    explanation: str = dspy.OutputField(
        desc="Explanation of why these changes were made and how they improve the video"
    )


class CritiqueEdit(dspy.Signature):
    """
    Review an edit made to a video script. Check if the edit is coherent, factually
    consistent with the original blog content, and accurately fulfills the user's request.

    Also verify that ONLY the requested scenes were changed and no others were modified.

    If the edit has problems, describe them. If it's good, say so.
    """

    original_script: str = dspy.InputField(desc="The original script before editing")
    edited_scenes: str = dspy.InputField(desc="The modified scenes (partial, not full script)")
    user_request: str = dspy.InputField(desc="What the user asked for")

    is_good: bool = dspy.OutputField(desc="True if the edit is satisfactory, False if it needs improvement")
    critique: str = dspy.OutputField(
        desc="Detailed critique of the edit. What's good, what's wrong, what should be fixed."
    )


class ScriptEditor:
    """
    DSPy-based script editor with reflexion (async).
    Returns ONLY changed scenes to preserve voiceover and Remotion code for untouched ones.
    """

    MAX_RETRIES = 2

    def __init__(self):
        ensure_dspy_configured()
        self._editor = dspy.ChainOfThought(EditScript)
        self._critic = dspy.ChainOfThought(CritiqueEdit)
        self.editor = dspy.asyncify(self._editor)
        self.critic = dspy.asyncify(self._critic)

    async def edit(
        self,
        current_scenes: list[dict],
        user_request: str,
        conversation_history: list[dict] | None = None,
    ) -> dict:
        """
        Edit the script with reflexion loop (async).
        Returns ONLY the changed scenes with their 'order' field intact.

        Returns:
            dict with 'changed_scenes' (list of dicts with 'order'), 'changes_made', 'explanation'
        """
        current_script_json = json.dumps(current_scenes, indent=2)
        history_str = self._format_history(conversation_history or [])

        # Initial edit
        edit_result = await self.editor(
            current_script=current_script_json,
            user_request=user_request,
            conversation_history=history_str,
        )

        updated_scenes_json = edit_result.updated_scenes
        changes_made = edit_result.changes_made
        explanation = edit_result.explanation

        # Reflexion loop: critique and retry if needed
        for attempt in range(self.MAX_RETRIES):
            critique_result = await self.critic(
                original_script=current_script_json,
                edited_scenes=updated_scenes_json,
                user_request=user_request,
            )

            if critique_result.is_good:
                break

            enhanced_request = (
                f"Original request: {user_request}\n\n"
                f"Previous attempt had issues: {critique_result.critique}\n\n"
                f"Please fix these issues while fulfilling the original request. "
                f"Remember: only return the scenes you changed, with their 'order' field."
            )

            edit_result = await self.editor(
                current_script=current_script_json,
                user_request=enhanced_request,
                conversation_history=history_str,
            )
            updated_scenes_json = edit_result.updated_scenes
            changes_made = edit_result.changes_made
            explanation = edit_result.explanation

        # Parse the changed scenes
        changed_scenes = self._parse_scenes(updated_scenes_json)

        return {
            "changed_scenes": changed_scenes,
            "changes_made": changes_made,
            "explanation": explanation,
        }

    def _format_history(self, history: list[dict]) -> str:
        """Format conversation history for the prompt."""
        if not history:
            return "No previous conversation."

        lines = []
        for msg in history[-10:]:
            role = msg.get("role", "user")
            content = msg.get("content", "")
            lines.append(f"{role}: {content}")
        return "\n".join(lines)

    def _parse_scenes(self, scenes_json: str) -> list[dict]:
        """Parse scenes JSON, handling potential formatting issues."""
        try:
            cleaned = scenes_json.strip()
            if cleaned.startswith("```"):
                lines = cleaned.split("\n")
                cleaned = "\n".join(lines[1:-1])

            scenes = json.loads(cleaned)
            if not isinstance(scenes, list):
                scenes = [scenes]
            return scenes
        except json.JSONDecodeError:
            return [{"order": 1, "title": "Modified Content", "narration": scenes_json[:500],
                      "visual_description": "Updated content", "suggested_images": [],
                      "duration_seconds": 15}]