Update README and streaming_turn_model.py to implement half-open interval ownership for fixed-grid AV frames. Adjust verification parameters to 1 for testing, and enhance documentation for clarity on audio slicing and frame selection. Add runtime checks to prevent overlapping temporal coordinates in open-stream intervals.

This commit is contained in:
Xin Wang
2026-08-17 22:45:17 +08:00
parent bde72487bb
commit 5ee371f0ae
2 changed files with 51 additions and 14 deletions

View File

@@ -450,8 +450,8 @@ python streaming_turn_model.py \
--start 0 --end 4 \ --start 0 --end 4 \
--video-fps 2 \ --video-fps 2 \
--strict-fixed-video-grid \ --strict-fixed-video-grid \
--verify-every 5 \ --verify-every 1 \
--verify-continuous-every 5 \ --verify-continuous-every 1 \
--flash-attention-2 \ --flash-attention-2 \
--output features/Session_1_open_first_4s.pt --output features/Session_1_open_first_4s.pt
``` ```
@@ -510,10 +510,12 @@ The sidecar selects the matching fixed-FPS MP4 and sets `N` to its exact
`--strict-fixed-video-grid` is enabled. `--strict-fixed-video-grid` is enabled.
In strict A+V mode the extractor does not delegate temporal frame selection to In strict A+V mode the extractor does not delegate temporal frame selection to
the generic video utility. It reads the already-fixed MP4 grid explicitly, the generic video utility. It reads the already-fixed MP4 grid explicitly and
uses only frames with timestamps `<= t`, and duplicates the most recent past assigns both video frames and audio samples with half-open intervals
frame when the visual tower needs an even frame count. Audio is loaded once at `[start, end)`. Thus a frame exactly on a two-second boundary belongs only to
16 kHz and sliced exactly at each causal boundary. The processor still receives the following block. It duplicates the most recent available frame when the
visual tower needs an even frame count. Audio is loaded once at 16 kHz and
sliced at the same causal boundary. The processor still receives
`use_audio_in_video=True`, so Qwen's audio/video token interleaving is retained. `use_audio_in_video=True`, so Qwen's audio/video token interleaving is retained.
Before a full session, run only the first four seconds and inspect both check Before a full session, run only the first four seconds and inspect both check
@@ -528,15 +530,17 @@ python streaming_turn_model.py \
--start 0 --end 4 \ --start 0 --end 4 \
--video-fps 2 \ --video-fps 2 \
--strict-fixed-video-grid \ --strict-fixed-video-grid \
--verify-every 5 \ --verify-every 1 \
--verify-continuous-every 5 \ --verify-continuous-every 1 \
--flash-attention-2 \ --flash-attention-2 \
--output features/Session_1_first_4s_check.pt --output features/Session_1_first_4s_check.pt
``` ```
Do not launch all sessions unless `cache_replay` cosine is nearly 1. The Do not launch all sessions unless `cache_replay` cosine is nearly 1. For
`continuous_window_semantics` cosine answers a different question and may be `open-stream-replay`, a materially lower `continuous_window_semantics` cosine
lower because media-turn boundaries change the representation. means that the custom payload stitching is not equivalent to one continuous
Qwen input; it must not be dismissed as a media-turn-boundary effect because
this mode intentionally keeps a single media turn open.
Video can be sampled at 2 FPS while predictions remain at 5 Hz. Audio and the Video can be sampled at 2 FPS while predictions remain at 5 Hz. Audio and the
causal boundary update every 200 ms; consecutive prediction times may use the causal boundary update every 200 ms; consecutive prediction times may use the
@@ -557,6 +561,7 @@ Each `.pt` file contains:
- `config`: step, native block, memory, FPS, and pixel settings; - `config`: step, native block, memory, FPS, and pixel settings;
- `verification`: cached-versus-exact numerical checks; - `verification`: cached-versus-exact numerical checks;
- `input_video_fps` and `video_sampling_grid`: sampling provenance; - `input_video_fps` and `video_sampling_grid`: sampling provenance;
- `media_interval_policy`: fixed-grid AV uses half-open `[start, end)` ownership;
- `logits`: optional `[N, 10, 3]` output if a trained head is supplied; - `logits`: optional `[N, 10, 3]` output if a trained head is supplied;
- `probabilities`: optional softmax probabilities. - `probabilities`: optional softmax probabilities.

View File

@@ -167,7 +167,13 @@ class FixedGridAV:
) )
def frames(self, start: float, end: float) -> list[Any]: def frames(self, start: float, end: float) -> list[Any]:
"""Return fixed-grid frames whose timestamps lie in [start, end].""" """Return fixed-grid frames whose timestamps lie in ``[start, end)``.
Half-open ownership is essential at native block boundaries. For
example, the frame at exactly 2.0 s belongs to ``[2.0, 4.0)`` and must
not also be committed in ``[0.0, 2.0)``. Audio slicing below follows
the same convention.
"""
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@@ -175,7 +181,7 @@ class FixedGridAV:
first = max(0, int(math.ceil(start * self.fps - 1e-7))) first = max(0, int(math.ceil(start * self.fps - 1e-7)))
last = min( last = min(
self.num_frames - 1, self.num_frames - 1,
int(math.floor(end * self.fps + 1e-7)), int(math.ceil(end * self.fps - 1e-7)) - 1,
) )
if first > last: if first > last:
# This can occur only when a caller starts between global visual # This can occur only when a caller starts between global visual
@@ -1255,11 +1261,32 @@ class OpenStreamReplayExtractor:
manual_audios=manual_audios, manual_audios=manual_audios,
video_fps=self.config.video_fps, video_fps=self.config.video_fps,
) )
return _open_media_pieces( pieces = _open_media_pieces(
self.model, self.model,
inputs, inputs,
use_audio_in_video=use_audio_in_video, use_audio_in_video=use_audio_in_video,
) )
# A standalone tail is at most one native 2 s block. Its temporal
# TMRoPE coordinates must therefore stay inside one 50-position chunk.
# This catches endpoint-inclusive frame selection (or another
# preprocessing regression) before overlapping coordinates are
# committed to the next block.
duration = interval[1] - interval[0]
if duration <= self.config.native_block_seconds + 1e-6:
temporal_positions = pieces.payload_position_ids[0]
temporal_span = int(
temporal_positions.max().item()
- temporal_positions.min().item()
)
positions_per_chunk = _tmrope_positions_per_chunk(self.model)
if temporal_span >= positions_per_chunk:
raise RuntimeError(
"One open-stream AV interval spans multiple native TMRoPE "
f"chunks: interval={interval}, temporal_span={temporal_span}, "
f"chunk_size={positions_per_chunk}. Check frame-boundary "
"ownership and audio preprocessing."
)
return pieces
def state_from_scratch( def state_from_scratch(
self, self,
@@ -1812,6 +1839,11 @@ def main() -> None:
if fixed_grid_av is not None if fixed_grid_av is not None
else "qwen_utility_adaptive_sampling" else "qwen_utility_adaptive_sampling"
), ),
"media_interval_policy": (
"half_open_[start,end)"
if fixed_grid_av is not None
else "qwen_utility_defined"
),
"open_stream_policy": ( "open_stream_policy": (
{ {
"commit": "completed_2s_av_payload_only", "commit": "completed_2s_av_payload_only",