-
-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathruntime.py
More file actions
196 lines (163 loc) · 7.3 KB
/
Copy pathruntime.py
File metadata and controls
196 lines (163 loc) · 7.3 KB
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
"""Apply a resolved tool to a prompt (build instruction → expand → de-ban)."""
from __future__ import annotations
import subprocess
import sys
from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlparse
import structlog
from gflow_cli.config import get_settings
from gflow_cli.tools.banned import strip_banned_keywords
from gflow_cli.tools.expander import ExpansionResult, PromptExpander, resolve_model
if TYPE_CHECKING:
from gflow_cli.tools.spec import DomainCategory, ToolConfig, ToolSpec
log = structlog.get_logger(__name__)
VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".mov", ".m4v", ".avi", ".flv", ".wmv"}
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"}
DEFAULT_CLAUDE_VIDEO_DIR = "C:/development/github/claude-video"
def _is_url(s: str) -> bool:
if s.startswith("-"):
return False
parsed = urlparse(s)
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
def _is_video_file(s: str) -> bool:
try:
p = Path(s)
return p.is_file() and p.suffix.lower() in VIDEO_EXTS
except Exception:
return False
def _is_image_file(s: str) -> bool:
try:
p = Path(s)
return p.is_file() and p.suffix.lower() in IMAGE_EXTS
except Exception:
return False
def _get_clean_name(prompt: str) -> str:
if _is_url(prompt):
parsed = urlparse(prompt)
path_parts = [p for p in parsed.path.split("/") if p]
if path_parts:
# e.g., for /reel/Dag0kcwMrX0/, return Dag0kcwMrX0
return path_parts[-1]
return "url_video"
else:
return Path(prompt).stem
def build_instruction(
config: ToolConfig,
style: str | None,
category: DomainCategory | None = None,
) -> str:
# The TOML system_template carries ONLY the formula (no trailing marker);
# build_instruction appends the user-prompt marker EXACTLY ONCE, after any
# domain vocabulary — so the domain and no-domain branches never duplicate it.
# ``category`` gates which same-named domain (image vs video) is selected.
parts = [config.system_template.rstrip()]
domain = config.domain(style, category)
if style is not None and domain is None:
log.warning("tool_unknown_style", style=style, category=category)
if domain is not None:
parts.append(f"Apply this {domain.name} style vocabulary: {domain.vocabulary}")
return "\n\n".join(parts) + "\n\nUser prompt: "
def _collect_frames(prompt: str) -> list[str]:
"""Return frame image paths from a video/URL prompt via watch.py, or [] on failure."""
watch_py = Path(DEFAULT_CLAUDE_VIDEO_DIR) / "scripts" / "watch.py"
if not watch_py.exists():
log.warning(
"watch_py_not_found",
path=str(watch_py),
reason="falling back to text-only reverse engineering",
)
return []
clean_name = _get_clean_name(prompt)
out_dir = Path("tmp/watch") / clean_name
out_dir.mkdir(parents=True, exist_ok=True)
cmd = [sys.executable, str(watch_py), prompt, "--no-whisper", "--out-dir", str(out_dir)]
log.info("running_watch_py", cmd=cmd)
log.info("saving_collateral_under", path=str(out_dir))
res = subprocess.run(cmd, capture_output=True, text=True, timeout=120) # noqa: S603
if res.returncode != 0:
log.warning("watch_py_failed", code=res.returncode, stderr=res.stderr)
return []
frames_dir = out_dir / "frames"
if not frames_dir.exists():
return []
frames = sorted(frames_dir.glob("*.jpg"))
if len(frames) > 5:
frames = [frames[int(i * len(frames) / 5)] for i in range(5)]
image_paths = [str(f.resolve()) for f in frames]
log.info("extracted_frames_for_analysis", count=len(image_paths))
return image_paths
def _apply_multimodal_reverse_engineering(
spec: ToolSpec,
prompt: str,
expander: PromptExpander,
) -> ExpansionResult | None:
"""Attempt multimodal reverse engineering; return result or None to fall through."""
log.info("multimodal_reverse_engineering_detected", prompt=prompt)
log.info("engine_inspired_by_claude_video_watch_skill")
try:
if _is_image_file(prompt):
image_paths: list[str] = [str(Path(prompt).resolve())]
else:
image_paths = _collect_frames(prompt)
if not image_paths:
return None
result = expander.expand_multimodal(prompt, image_paths)
if not result.was_expanded:
return None
cleaned, removed = strip_banned_keywords(result.expanded, spec.config.banned_keywords)
if removed:
log.info("tool_banned_keywords_stripped", tool=spec.name, removed=removed)
return ExpansionResult(original=result.original, expanded=cleaned, was_expanded=True)
except Exception as e: # noqa: BLE001
log.warning("multimodal_reverse_engineering_error", error=str(e))
return None
def apply_tool(
spec: ToolSpec,
prompt: str,
options: Mapping[str, str],
*,
category: DomainCategory | None = None,
expander: PromptExpander | None = None,
) -> ExpansionResult:
style = options.get("style")
instruction = build_instruction(spec.config, style, category)
if expander is None:
settings = get_settings()
api_key = settings.llm_api_key.get_secret_value() if settings.llm_api_key else None
expander = PromptExpander(
api_key,
base_url=settings.llm_base_url,
# Precedence lives in resolve_model so this and the provenance
# record in invocation.py cannot drift: TOML pin (the tool author
# knew what that tool needs) > GFLOW_CLI_LLM_MODEL > the default
# endpoint's own default > None (a chosen gateway picks). The
# builtins deliberately pin nothing -- a hardcoded vendor model name
# is what stopped a non-Google gateway from working at all.
model=resolve_model(spec.config.model, settings.llm_model, settings.llm_base_url),
system_instruction=instruction,
max_input_chars=spec.config.max_input_chars,
max_output_chars=spec.config.max_output_chars,
)
# 1. Attempt multimodal reverse engineering for qualifying inputs
if spec.name == "reverse-engineer" and (
_is_url(prompt) or _is_video_file(prompt) or _is_image_file(prompt)
):
result = _apply_multimodal_reverse_engineering(spec, prompt, expander)
if result is not None:
return result
# Do NOT fall through to text expansion here: `prompt` is a URL or a
# file path, so expanding it would ask the model to embellish a path
# string and return a confident, useless prompt that still bills a
# full generation. Degrade to the original instead.
log.warning("multimodal_reverse_engineering_unavailable", prompt=prompt)
return ExpansionResult(original=prompt, expanded=prompt, was_expanded=False)
# 2. Fall back to standard text-only expansion
result = expander.expand(prompt)
if not result.was_expanded:
return result
cleaned, removed = strip_banned_keywords(result.expanded, spec.config.banned_keywords)
if removed:
log.info("tool_banned_keywords_stripped", tool=spec.name, removed=removed)
return ExpansionResult(original=result.original, expanded=cleaned, was_expanded=True)