Buckets:
stzhao/rice-range / tmp /video-velocity-model /data_processing /VidaForge /viewers /viewer_common.py
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| from collections.abc import Sequence | |
| from pathlib import Path | |
| import altair as alt | |
| import pandas as pd | |
| import streamlit as st | |
| from vidaforge.common import join_data_dir, join_raw_dir | |
| from vidaforge.index import load_parquet | |
| DEFAULT_PROJECT_DIR = Path( | |
| os.environ.get( | |
| "DATA_DIR", | |
| str(Path.cwd() / "examples" / "vidaforge_output"), | |
| ) | |
| ) | |
| DEFAULT_DATAFRAME_PREVIEW_ROWS = 1000 | |
| DEFAULT_JSON_PREVIEW_ITEMS = 50 | |
| DEFAULT_JSON_STRING_MAX_CHARS = 2000 | |
| LARGE_JSON_KEYS = { | |
| "task_outputs", | |
| "failed_task_outputs", | |
| "failed_examples", | |
| } | |
| def load_rows( | |
| metadata_path: str, | |
| *, | |
| unit: str | None = None, | |
| columns: tuple[str, ...] | None = None, | |
| ) -> list[dict[str, object]]: | |
| return load_parquet( | |
| metadata_path, | |
| unit=unit, | |
| columns=list(columns) if columns is not None else None, | |
| ) | |
| def load_summary(summary_path: str) -> dict[str, object]: | |
| path = Path(summary_path).expanduser().resolve() | |
| if not path.exists() or not path.is_file(): | |
| return {} | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| def format_elapsed_seconds(value: object) -> str: | |
| if not isinstance(value, int | float): | |
| return "-" | |
| seconds = float(value) | |
| if seconds < 0: | |
| return "-" | |
| if seconds < 60: | |
| return f"{seconds:.1f}s" | |
| if seconds < 3600: | |
| minutes = int(seconds // 60) | |
| remain = int(round(seconds % 60)) | |
| return f"{minutes}m {remain:02d}s" | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| return f"{hours}h {minutes:02d}m" | |
| def format_bytes(value: object) -> str: | |
| if not isinstance(value, int | float): | |
| return "-" | |
| size = float(value) | |
| if size < 0: | |
| return "-" | |
| units = ("B", "KB", "MB", "GB", "TB", "PB") | |
| unit_index = 0 | |
| while size >= 1024.0 and unit_index < len(units) - 1: | |
| size /= 1024.0 | |
| unit_index += 1 | |
| if unit_index == 0: | |
| return f"{int(size)} {units[unit_index]}" | |
| return f"{size:.2f} {units[unit_index]}" | |
| def load_file_sizes(file_paths: tuple[str, ...]) -> list[int | None]: | |
| sizes: list[int | None] = [] | |
| for value in file_paths: | |
| path_str = str(value or "").strip() | |
| if not path_str: | |
| sizes.append(None) | |
| continue | |
| try: | |
| path = join_data_dir(path_str) | |
| if not path.exists() or not path.is_file(): | |
| sizes.append(None) | |
| continue | |
| sizes.append(path.stat().st_size) | |
| except OSError: | |
| sizes.append(None) | |
| return sizes | |
| def list_run_ids(project_dir: str, stage_dir: str, step_dir: str) -> list[str]: | |
| meta_dir = Path(project_dir).expanduser().resolve() / "meta" / stage_dir / step_dir | |
| if not meta_dir.exists() or not meta_dir.is_dir(): | |
| return [] | |
| run_ids: list[str] = [] | |
| for path in meta_dir.iterdir(): | |
| if not path.is_dir(): | |
| continue | |
| if path.name.startswith("run_id="): | |
| run_ids.append(path.name.removeprefix("run_id=")) | |
| continue | |
| run_ids.append(path.name) | |
| return sorted(run_ids, reverse=True) | |
| def resolve_metadata_path( | |
| project_dir: str, | |
| stage_dir: str, | |
| step_dir: str, | |
| run_id: str, | |
| override: str, | |
| ) -> Path: | |
| override_path = Path(override).expanduser() | |
| if override.strip(): | |
| return override_path.resolve() | |
| base_dir = Path(project_dir).expanduser().resolve() / "meta" / stage_dir / step_dir | |
| legacy_path = base_dir / f"run_id={run_id}" | |
| if legacy_path.exists(): | |
| return legacy_path | |
| return base_dir / run_id | |
| def resolve_summary_path(metadata_path: Path) -> Path: | |
| path = metadata_path.expanduser().resolve() | |
| if path.is_dir(): | |
| return path / "summary.json" | |
| return path.parent / "summary.json" | |
| def rows_to_dataframe(rows: Sequence[dict[str, object]]) -> pd.DataFrame: | |
| frame = pd.DataFrame.from_records(rows) | |
| for column in ( | |
| "filesize_bytes", | |
| "filesize_mb", | |
| "duration_sec", | |
| "fps", | |
| "width", | |
| "height", | |
| "bit_rate", | |
| "probe_ok", | |
| "probe_elapsed_ms", | |
| "filter_keep", | |
| ): | |
| if column in frame.columns: | |
| frame[column] = pd.to_numeric(frame[column], errors="coerce") | |
| return frame | |
| def dataframe_preview( | |
| frame: pd.DataFrame, | |
| *, | |
| max_rows: int = DEFAULT_DATAFRAME_PREVIEW_ROWS, | |
| ) -> pd.DataFrame: | |
| if max_rows <= 0 or len(frame) <= max_rows: | |
| return frame | |
| return frame.head(max_rows) | |
| def render_dataframe_preview( | |
| frame: pd.DataFrame, | |
| *, | |
| max_rows: int = DEFAULT_DATAFRAME_PREVIEW_ROWS, | |
| **kwargs: object, | |
| ) -> None: | |
| preview = dataframe_preview(frame, max_rows=max_rows) | |
| if len(preview) < len(frame): | |
| st.caption(f"Showing the first {len(preview):,} / {len(frame):,} rows to avoid loading too much data in the browser.") | |
| st.dataframe(preview, **kwargs) | |
| def _compact_json_for_display(value: object) -> object: | |
| if isinstance(value, dict): | |
| compacted: dict[str, object] = {} | |
| for key, item in value.items(): | |
| key_text = str(key) | |
| if key_text in LARGE_JSON_KEYS: | |
| if isinstance(item, list | tuple): | |
| compacted[key_text] = { | |
| "omitted": True, | |
| "count": len(item), | |
| "preview": [ | |
| _compact_json_for_display(element) | |
| for element in list(item)[:DEFAULT_JSON_PREVIEW_ITEMS] | |
| ], | |
| } | |
| else: | |
| compacted[key_text] = "<omitted large field>" | |
| continue | |
| compacted[key_text] = _compact_json_for_display(item) | |
| return compacted | |
| if isinstance(value, list | tuple): | |
| items = list(value) | |
| if len(items) <= DEFAULT_JSON_PREVIEW_ITEMS: | |
| return [_compact_json_for_display(item) for item in items] | |
| return { | |
| "omitted": True, | |
| "count": len(items), | |
| "preview": [ | |
| _compact_json_for_display(item) | |
| for item in items[:DEFAULT_JSON_PREVIEW_ITEMS] | |
| ], | |
| } | |
| if isinstance(value, str) and len(value) > DEFAULT_JSON_STRING_MAX_CHARS: | |
| return value[:DEFAULT_JSON_STRING_MAX_CHARS] + "... <truncated>" | |
| return value | |
| def render_json_preview(value: object) -> None: | |
| st.json(_compact_json_for_display(value)) | |
| def ensure_size_columns( | |
| frame: pd.DataFrame, | |
| *, | |
| size_bytes_col: str, | |
| size_mb_col: str, | |
| path_col: str | None = None, | |
| ) -> pd.DataFrame: | |
| enriched = frame.copy() | |
| if size_bytes_col in enriched.columns: | |
| enriched[size_bytes_col] = pd.to_numeric(enriched[size_bytes_col], errors="coerce") | |
| elif size_mb_col in enriched.columns: | |
| size_mb = pd.to_numeric(enriched[size_mb_col], errors="coerce") | |
| enriched[size_mb_col] = size_mb | |
| enriched[size_bytes_col] = size_mb * 1024 * 1024 | |
| elif path_col and path_col in enriched.columns: | |
| sizes = load_file_sizes(tuple(enriched[path_col].fillna("").astype(str).tolist())) | |
| enriched[size_bytes_col] = pd.to_numeric(pd.Series(sizes, index=enriched.index), errors="coerce") | |
| else: | |
| enriched[size_bytes_col] = pd.Series([pd.NA] * len(enriched), index=enriched.index, dtype="Float64") | |
| if size_mb_col not in enriched.columns: | |
| enriched[size_mb_col] = pd.to_numeric(enriched[size_bytes_col], errors="coerce") / (1024 * 1024) | |
| else: | |
| enriched[size_mb_col] = pd.to_numeric(enriched[size_mb_col], errors="coerce") | |
| return enriched | |
| def summarize_total_size(frame: pd.DataFrame, *, size_bytes_col: str) -> str: | |
| if size_bytes_col not in frame.columns: | |
| return "-" | |
| series = pd.to_numeric(frame[size_bytes_col], errors="coerce").dropna() | |
| series = series[series >= 0] | |
| if series.empty: | |
| return "-" | |
| return format_bytes(float(series.sum())) | |
| def render_bar_chart( | |
| frame: pd.DataFrame, | |
| *, | |
| category_col: str, | |
| value_col: str, | |
| title: str, | |
| height: int = 320, | |
| sort: str | list[str] | None = None, | |
| label_angle: int = 0, | |
| ) -> None: | |
| chart = ( | |
| alt.Chart(frame) | |
| .mark_bar() | |
| .encode( | |
| x=alt.X( | |
| f"{category_col}:N", | |
| sort=sort, | |
| axis=alt.Axis( | |
| labelAngle=label_angle, | |
| labelLimit=220, | |
| labelOverlap=False, | |
| ), | |
| ), | |
| y=alt.Y(f"{value_col}:Q", title="count"), | |
| tooltip=[category_col, value_col], | |
| ) | |
| .properties(height=height) | |
| ) | |
| st.caption(title) | |
| st.altair_chart(chart, width="stretch") | |
| def add_fixed_range_chart( | |
| frame: pd.DataFrame, | |
| column: str, | |
| title: str, | |
| *, | |
| bins: list[float], | |
| labels: list[str], | |
| ) -> None: | |
| if column not in frame.columns: | |
| st.info(f"{title}: missing field `{column}`") | |
| return | |
| series = pd.to_numeric(frame[column], errors="coerce").dropna() | |
| series = series[series >= 0] | |
| if series.empty: | |
| st.info(f"{title}: no data to summarize") | |
| return | |
| counts = ( | |
| pd.cut(series, bins=bins, labels=labels, right=False, include_lowest=True) | |
| .value_counts(sort=False) | |
| .rename_axis("range") | |
| .reset_index(name="count") | |
| ) | |
| counts = counts[counts["count"] > 0] | |
| if counts.empty: | |
| st.info(f"{title}: no data to summarize") | |
| return | |
| render_bar_chart(counts, category_col="range", value_col="count", title=title) | |
| def add_value_counts_chart( | |
| frame: pd.DataFrame, | |
| column: str, | |
| title: str, | |
| *, | |
| top_k: int | None = None, | |
| empty_label: str = "unknown", | |
| height: int = 320, | |
| ) -> None: | |
| if column not in frame.columns: | |
| st.info(f"{title}: missing field `{column}`") | |
| return | |
| counts = ( | |
| frame[column] | |
| .fillna("") | |
| .astype(str) | |
| .replace("", empty_label) | |
| .value_counts() | |
| .rename_axis(column) | |
| .reset_index(name="count") | |
| ) | |
| if top_k is not None: | |
| counts = counts.head(top_k) | |
| if counts.empty: | |
| st.info(f"{title}: no data to summarize") | |
| return | |
| render_bar_chart( | |
| counts, | |
| category_col=column, | |
| value_col="count", | |
| title=title, | |
| height=height, | |
| ) | |
| def add_resolution_chart(frame: pd.DataFrame, top_k: int = 20) -> None: | |
| if "width" not in frame.columns or "height" not in frame.columns: | |
| st.info("Resolution Distribution: missing `width` or `height` field") | |
| return | |
| width = pd.to_numeric(frame["width"], errors="coerce") | |
| height = pd.to_numeric(frame["height"], errors="coerce") | |
| valid = frame[width.notna() & height.notna()].copy() | |
| if valid.empty: | |
| st.info("Resolution Distribution: no data to summarize") | |
| return | |
| valid["resolution"] = ( | |
| width[width.notna()].astype(int).astype(str) | |
| + "x" | |
| + height[height.notna()].astype(int).astype(str) | |
| ) | |
| counts = ( | |
| valid["resolution"] | |
| .value_counts() | |
| .head(top_k) | |
| .rename_axis("resolution") | |
| .reset_index(name="count") | |
| ) | |
| render_bar_chart( | |
| counts, | |
| category_col="resolution", | |
| value_col="count", | |
| title="Resolution Distribution(Top K)", | |
| height=360, | |
| sort="-y", | |
| label_angle=45, | |
| ) | |
| def add_fps_chart(frame: pd.DataFrame, top_k: int = 12) -> None: | |
| if "fps" not in frame.columns: | |
| st.info("FPS Distribution (Top K): missing field `fps`") | |
| return | |
| series = pd.to_numeric(frame["fps"], errors="coerce").dropna() | |
| series = series[series > 0] | |
| if series.empty: | |
| st.info("FPS Distribution (Top K): no data to summarize") | |
| return | |
| counts = ( | |
| series.round(2) | |
| .map(lambda value: f"{value:g}") | |
| .value_counts() | |
| .head(top_k) | |
| .rename_axis("fps") | |
| .reset_index(name="count") | |
| ) | |
| render_bar_chart(counts, category_col="fps", value_col="count", title="FPS Distribution (Top K)") | |
| def apply_text_probe_filters( | |
| frame: pd.DataFrame, | |
| *, | |
| keyword: str, | |
| probe_filter: str, | |
| extra_text_cols: Sequence[str] = (), | |
| ) -> pd.DataFrame: | |
| filtered = frame | |
| if keyword: | |
| keyword_lower = keyword.lower() | |
| text_cols = [ | |
| col | |
| for col in ( | |
| "video_path", | |
| "raw_path", | |
| "codec", | |
| "probe_error", | |
| *extra_text_cols, | |
| ) | |
| if col in filtered.columns | |
| ] | |
| if text_cols: | |
| mask = pd.Series(False, index=filtered.index) | |
| for col in text_cols: | |
| mask = mask | filtered[col].fillna("").astype(str).str.lower().str.contains( | |
| keyword_lower, regex=False | |
| ) | |
| filtered = filtered[mask] | |
| if probe_filter != "all" and "probe_ok" in filtered.columns: | |
| if probe_filter == "ok": | |
| filtered = filtered[filtered["probe_ok"] == 1] | |
| elif probe_filter == "failed": | |
| filtered = filtered[filtered["probe_ok"] != 1] | |
| return filtered.reset_index(drop=True) | |
| def paginate(frame: pd.DataFrame, page: int, per_page: int) -> pd.DataFrame: | |
| start = (page - 1) * per_page | |
| end = start + per_page | |
| return frame.iloc[start:end] | |
| def show_samples( | |
| frame: pd.DataFrame, | |
| *, | |
| per_page: int, | |
| columns: int, | |
| detail_fields: Sequence[str], | |
| media_path_fields: Sequence[str] = ("video_path",), | |
| raw_media_path_fields: Sequence[str] = (), | |
| browse_order: str = "ordered", | |
| widget_key_prefix: str = "samples", | |
| show_local_paths: bool = False, | |
| ) -> None: | |
| total = len(frame) | |
| if total == 0: | |
| st.warning("No samples match the current filters.") | |
| return | |
| display_frame = frame | |
| if browse_order == "shuffled": | |
| display_frame = frame.sample(frac=1.0, random_state=0).reset_index(drop=True) | |
| raw_media_fields = set(raw_media_path_fields) | |
| pages = math.ceil(total / per_page) | |
| page = int( | |
| st.number_input( | |
| "Page", | |
| min_value=1, | |
| max_value=pages, | |
| value=1, | |
| step=1, | |
| key=f"{widget_key_prefix}_page", | |
| ) | |
| ) | |
| st.caption(f"Page {page}/{pages}") | |
| current = paginate(display_frame, page, per_page) | |
| for i in range(0, len(current), columns): | |
| block = current.iloc[i : i + columns] | |
| cols = st.columns(len(block)) | |
| for col, (_, row) in zip(cols, block.iterrows()): | |
| with col: | |
| media_path = "" | |
| media_field = "" | |
| fallback_media_path = "" | |
| fallback_media_field = "" | |
| for field in media_path_fields: | |
| value = str(row.get(field, "") or "").strip() | |
| if not value: | |
| continue | |
| if not fallback_media_path: | |
| fallback_media_path = value | |
| fallback_media_field = field | |
| if value.startswith(("http://", "https://")): | |
| media_path = value | |
| media_field = field | |
| break | |
| candidate = ( | |
| join_raw_dir(value) | |
| if field in raw_media_fields | |
| else join_data_dir(value) | |
| ) | |
| if candidate.exists(): | |
| media_path = str(candidate) | |
| media_field = field | |
| break | |
| if media_path: | |
| try: | |
| st.video(media_path, autoplay=False) | |
| st.caption(f"{media_field}: {Path(media_path).name}") | |
| except Exception as exc: # noqa: BLE001 | |
| display_path = media_path if show_local_paths else Path(media_path).name | |
| st.warning(f"Video file cannot be opened and was skipped: {display_path}") | |
| st.caption(str(exc) if show_local_paths else type(exc).__name__) | |
| elif fallback_media_path: | |
| display_path = ( | |
| fallback_media_path | |
| if show_local_paths | |
| else Path(fallback_media_path).name | |
| ) | |
| st.caption(f"{fallback_media_field} missing: {display_path}") | |
| detail = { | |
| key: value | |
| for key, value in row.to_dict().items() | |
| if key in detail_fields and pd.notna(value) | |
| } | |
| with st.expander("meta", expanded=False): | |
| st.json(detail) | |
| def render_metadata_selection( | |
| *, | |
| stage_dir: str, | |
| step_dir: str, | |
| title: str, | |
| include_filter_scope: bool, | |
| include_probe_filter: bool = True, | |
| include_browse_layout_controls: bool = True, | |
| include_raw_dir: bool = False, | |
| load_frame: bool = True, | |
| load_summary_data: bool = True, | |
| ) -> dict[str, object] | None: | |
| st.subheader(title) | |
| default_project_dir = str(DEFAULT_PROJECT_DIR) | |
| widget_prefix = f"{stage_dir}_{step_dir}" | |
| with st.sidebar: | |
| st.subheader(title) | |
| show_local_paths = st.toggle( | |
| "Show filesystem paths", | |
| value=False, | |
| key=f"{widget_prefix}_show_local_paths", | |
| help="Disabled by default to avoid exposing local paths in screenshots or recordings.", | |
| ) | |
| path_input_type = "default" if show_local_paths else "password" | |
| project_dir = st.text_input( | |
| "Project data root", | |
| value=default_project_dir, | |
| key=f"{widget_prefix}_project_dir", | |
| type=path_input_type, | |
| ) | |
| os.environ["DATA_DIR"] = str(Path(project_dir).expanduser().resolve()) | |
| if include_raw_dir: | |
| default_raw_dir = os.environ.get( | |
| "RAW_DIR", | |
| str(Path.cwd() / "examples" / "raw_videos"), | |
| ) | |
| raw_dir = st.text_input( | |
| "Raw data root", | |
| value=default_raw_dir, | |
| key=f"{widget_prefix}_raw_dir", | |
| help="Root directory used to resolve raw_path values.", | |
| type=path_input_type, | |
| ) | |
| os.environ["RAW_DIR"] = str(Path(raw_dir).expanduser().resolve()) | |
| available_run_ids = list_run_ids(project_dir, stage_dir, step_dir) | |
| run_id_options = available_run_ids or [""] | |
| run_id = st.selectbox( | |
| "run_id", | |
| run_id_options, | |
| index=0, | |
| disabled=not available_run_ids, | |
| key=f"{widget_prefix}_run_id", | |
| ) | |
| metadata_override = st.text_input( | |
| "Metadata path override (optional)", | |
| value="", | |
| key=f"{widget_prefix}_metadata_override", | |
| type=path_input_type, | |
| ) | |
| if include_browse_layout_controls: | |
| per_page = st.selectbox("Videos per page", [4, 8, 12, 16, 24], index=2, key=f"{widget_prefix}_per_page") | |
| columns = st.selectbox("Columns", [1, 2, 3, 4], index=1, key=f"{widget_prefix}_columns") | |
| else: | |
| per_page = 12 | |
| columns = 2 | |
| browse_order = st.selectbox( | |
| "Sample order", | |
| ("ordered", "shuffled"), | |
| index=0, | |
| format_func=lambda value: "ordered" if value == "ordered" else "shuffled", | |
| key=f"{widget_prefix}_browse_order", | |
| ) | |
| keyword = st.text_input("Keyword filter", value="", key=f"{widget_prefix}_keyword").strip() | |
| probe_filter = "all" | |
| if include_probe_filter: | |
| probe_filter = st.selectbox( | |
| "Probe filter", | |
| ("all", "ok", "failed"), | |
| index=0, | |
| key=f"{widget_prefix}_probe_filter", | |
| ) | |
| filter_scope = "all" | |
| if include_filter_scope: | |
| filter_scope = st.selectbox( | |
| "Filter result", | |
| ("all", "keep", "reject"), | |
| index=0, | |
| key=f"{widget_prefix}_filter_scope", | |
| ) | |
| extra_sidebar_container = st.container() | |
| refresh = st.button( | |
| "Refresh cache", | |
| key=f"{widget_prefix}_refresh", | |
| help="Clear Streamlit cache and reload current metadata and summary files.", | |
| ) | |
| if not available_run_ids and not metadata_override.strip(): | |
| st.error("No run_id directory found for this step. Check the data directory, or use the metadata path override.") | |
| return None | |
| metadata_path = resolve_metadata_path(project_dir, stage_dir, step_dir, run_id, metadata_override) | |
| if show_local_paths: | |
| metadata_path_label = str(metadata_path) | |
| else: | |
| try: | |
| metadata_path_label = str( | |
| metadata_path.relative_to(Path(project_dir).expanduser().resolve()) | |
| ) | |
| except ValueError: | |
| metadata_path_label = metadata_path.name | |
| st.caption(f"Current metadata path: `{metadata_path_label}`") | |
| if refresh: | |
| load_rows.clear() | |
| load_summary.clear() | |
| if not metadata_path.exists(): | |
| st.error("Metadata path does not exist. Check run_id or override the path manually.") | |
| return None | |
| frame = None | |
| if load_frame: | |
| rows = load_rows(str(metadata_path)) | |
| if not rows: | |
| st.warning("No records were loaded.") | |
| return None | |
| frame = rows_to_dataframe(rows) | |
| return { | |
| "metadata_path": metadata_path, | |
| "summary": load_summary(str(resolve_summary_path(metadata_path))) if load_summary_data else {}, | |
| "frame": frame, | |
| "per_page": per_page, | |
| "columns": columns, | |
| "browse_order": browse_order, | |
| "keyword": keyword, | |
| "probe_filter": probe_filter, | |
| "filter_scope": filter_scope, | |
| "extra_sidebar_container": extra_sidebar_container, | |
| "refresh": refresh, | |
| "show_local_paths": show_local_paths, | |
| } | |
Xet Storage Details
- Size:
- 22.4 kB
- Xet hash:
- 439863f9d4abcd5bf6466ba984b087c10ecd77916ef28c744cbe9a7a6404232d
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.