feat: timed phrases use square brackets so shortcut parentheses can sit inside

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-20 00:49:16 +03:00
co-authored by Claude Opus 5
parent aa4efe3248
commit 25e5cc8de0
4 changed files with 15 additions and 15 deletions
+1 -1
View File
@@ -4456,7 +4456,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
self._v2_tally_encode(purpose, 0.0, cached=True)
return cached
_encode_started = time.perf_counter()
# Timed phrases `(walks left@2.0-3.5)` are a sampler instruction, not prompt text.
# Timed phrases `[walks left@2.0-3.5]` are a sampler instruction, not prompt text.
# Stripped for EVERY encoder (Qwen or Gemma would read them as punctuation); only H3
# can act on them, and the sampler says so when it cannot.
try:
+5 -5
View File
@@ -66,11 +66,11 @@ def parse(text: str):
return "".join(out), spans
# (phrase@2.0-3.5) / (phrase:1.5@2.0-3.5): a TIMED phrase. Seconds from the scene start.
# The phrase may contain `:` and `@` (shortcut replacements do): only a `:number` directly
# before the `@t0-t1)` tail is a weight. Parentheses inside the phrase are still not allowed.
# [phrase@2.0-3.5] / [phrase:1.5@2.0-3.5]: a TIMED phrase. Seconds from the scene start.
# Square brackets so a shortcut replacement's parentheses and colons can sit inside; only a
# `:number` directly before the `@t0-t1]` tail is a weight.
_TIMED = re.compile(
r"(?<!\\)\(([^()]*?)(?::\s*(-?\d+(?:\.\d+)?))?\s*@\s*(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*\)")
r"(?<!\\)\[([^\[\]]*?)(?::\s*(-?\d+(?:\.\d+)?))?\s*@\s*(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*\]")
# H3's latent time grid: latent frame k spans FRAME_PER_TOKEN[k % 5] pixel frames at 24 fps.
FPS = 24
FRAME_PER_TOKEN = (1, 4, 4, 4, 4)
@@ -79,7 +79,7 @@ FRAME_PER_TOKEN = (1, 4, 4, 4, 4)
def parse_timed(text: str):
"""Strip timed-phrase syntax. -> (clean_text, [(start_char, end_char, weight, t0, t1)]).
The model must never see `@2.0-3.5`: Qwen would read it as punctuation. Spans index
The model must never see `[…@2.0-3.5]`: Qwen would read it as punctuation. Spans index
into `clean_text` (same contract as `parse`). A window with t1 <= t0 is dropped.
"""
if not text or "@" not in text:
+3 -3
View File
@@ -256,7 +256,7 @@
let composeTextarea = null;
let varsOpen = false; // Variables panel expanded?
let varHintEl = null; // undeclared / cycle hint line under the prompt
let timedHintEl = null; // timed phrases `(walks left@2.0-3.5)` found in the prompt
let timedHintEl = null; // timed phrases `[walks left@2.0-3.5]` found in the prompt
// ── templates bar (above the global prompt) ─────────────────────────────────────
const TPL_NONE = "__none__";
@@ -438,9 +438,9 @@
return cycles;
}
// Timed phrases: `(phrase@t0-t1)` / `(phrase:w@t0-t1)`, seconds from the scene start. Mirrors
// Timed phrases: `[phrase@t0-t1]` / `[phrase:w@t0-t1]`, seconds from the scene start. Mirrors
// h3_token_weights._TIMED — the phrase is only allowed to shape the video inside its window.
const TIMED_RE = /(?<!\\)\(([^()]*?)(?::\s*(-?\d+(?:\.\d+)?))?\s*@\s*(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*\)/g;
const TIMED_RE = /(?<!\\)\[([^\[\]]*?)(?::\s*(-?\d+(?:\.\d+)?))?\s*@\s*(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*\]/g;
function updateTimedHint() {
if (!timedHintEl) return;
const txt = composeTextarea ? composeTextarea.value : "";
+6 -6
View File
@@ -801,13 +801,13 @@ def test_variability_out_of_range_is_clamped_not_inverted(refiner, monkeypatch):
# --- timed phrases -----------------------------------------------------------------
def test_timed_syntax_is_stripped_and_windows_kept():
clean, spans = tw.parse_timed("a man (walks left@1.0-2.5) then (waves:1.5 @ 3-4) (x:1.2)")
clean, spans = tw.parse_timed("a man [walks left@1.0-2.5] then [waves:1.5 @ 3-4] (x:1.2)")
assert clean == "a man walks left then waves (x:1.2)"
assert spans == [(6, 16, 1.0, 1.0, 2.5), (22, 27, 1.5, 3.0, 4.0)]
def test_an_empty_window_is_dropped():
assert tw.parse_timed("(walks@2-2)")[1] == []
assert tw.parse_timed("[walks@2-2]")[1] == []
def test_a_window_maps_to_whole_latent_frames():
@@ -869,7 +869,7 @@ def test_timed_and_untimed_spans_stack_and_a_window_weight_is_a_boost():
assert outside[0, 0, 0, 4] == tw.MASKED_BIAS
def test_a_phrase_may_contain_colons_and_at_signs():
clean, spans = tw.parse_timed("(camera: slow push-in, mail@x.com @1-2) (fast:0.5@2-3)")
assert clean == "camera: slow push-in, mail@x.com fast"
assert spans == [(0, 32, 1.0, 1.0, 2.0), (33, 37, 0.5, 2.0, 3.0)]
def test_a_phrase_may_contain_parentheses_colons_and_at_signs():
clean, spans = tw.parse_timed("[camera: (slow) push-in, mail@x.com @1-2] [fast:0.5@2-3]")
assert clean == "camera: (slow) push-in, mail@x.com fast"
assert spans == [(0, 34, 1.0, 1.0, 2.0), (35, 39, 0.5, 2.0, 3.0)]