Spaces:
Running
Running
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}]
|