David Baba
backend
0437edf
Raw
History Blame Contribute Delete
6.07 kB
"""Background entrypoint: validate inputs, run the mode's strategy, assemble the result.
The whole pipeline is: validate → strategy.detect (per-product boxes) → build_product_result
per product (+ scene-cut split / cut-frame export) → enrich mentions → store.
"""
from ..jobs import store
from ..mentions import (
count_keywords,
count_keywords_breakdown,
count_ocr_mentions,
parse_keywords,
transcribe,
)
from ..schemas import AnalysisResult, Box, DetectionMode, JobStatus, ProductResult
from .registry import BuildOpts, owlv2_needs_images, requires_name, requires_reference, strategy_for
from .strategies import ProductInput
from .tracks import build_product_result, detect_cut_frames
from .video import probe_duration
def _validate(mode: DetectionMode, products: list[ProductInput], opts: BuildOpts) -> None:
if not products:
raise ValueError("At least one product is required.")
if requires_name(mode) and any(not p.name.strip() for p in products):
raise ValueError(f"{mode.value} needs a name for every product.")
needs_ref = requires_reference(mode) or owlv2_needs_images(mode, opts)
if needs_ref and any(not p.exemplar_paths for p in products):
raise ValueError(f"{mode.value} needs a reference image for every product.")
def run_analysis(
job_id: str,
video_path: str,
products: list[ProductInput],
caption: str,
mention_keywords: str,
mode: DetectionMode,
split_on_cut: bool = False,
dino_variant: str = "v2",
owl_ref_type: str = "text",
owl_dino: str = "none",
) -> None:
"""Background entrypoint. Updates the job store in place."""
store.set_status(job_id, JobStatus.running)
# --- DEBUG: no-op mode → dummy result (delete this block to remove) ------ #
if mode == DetectionMode.none:
store.set_done(job_id, _dummy_result())
return
# ------------------------------------------------------------------------- #
try:
opts = BuildOpts(
dino_variant=dino_variant, owl_ref_type=owl_ref_type, owl_dino=owl_dino
)
_validate(mode, products, opts)
strategy = strategy_for(mode, opts)
duration = probe_duration(video_path)
fps = strategy.fps
per_product = strategy.detect(video_path, products) # {name: {frame: [Box]}}
# Slider modes re-filter/recount in the UI, so they own the scene-cut split too:
# hand the UI the cut boundaries and let it split at any threshold.
specs = strategy.score_specs
is_slider = bool(specs)
cut_frames: list[int] = []
if split_on_cut and is_slider:
union = sorted({int(k) for boxes in per_product.values() for k in boxes})
cut_frames = detect_cut_frames(video_path, fps, union)
result = AnalysisResult(
fps=float(fps),
duration_sec=duration,
mode=mode,
products=[
build_product_result(
p.name, per_product.get(p.name, {}), fps, duration, video_path,
split_on_cut and not is_slider, # backend split only for non-slider modes
)
for p in products
],
brand_name=", ".join(p.name for p in products) or None,
score_specs=specs,
cut_frames=cut_frames,
)
_enrich_mentions(
result, video_path, caption, [p.name for p in products], mention_keywords
)
store.set_done(job_id, result)
except Exception as exc: # noqa: BLE001 — surface any failure to the client
store.set_error(job_id, f"{type(exc).__name__}: {exc}")
def _enrich_mentions(
result: AnalysisResult,
video_path: str,
caption: str,
product_names: list[str],
mention_keywords: str,
) -> None:
"""Phase 4: transcript + mention counts, driven by the dedicated keyword list (falling
back to the product names). Each step degrades to None on failure so a flaky model never
loses the visual analysis."""
keywords = parse_keywords(mention_keywords) or [
n.strip() for n in product_names if n.strip()
]
if keywords:
result.caption_mentions = count_keywords(caption, keywords)
result.caption_mention_counts = count_keywords_breakdown(caption, keywords)
try:
result.transcript = transcribe(video_path)
if keywords:
result.audio_mentions = count_keywords(result.transcript, keywords)
result.audio_mention_counts = count_keywords_breakdown(
result.transcript, keywords
)
except Exception: # noqa: BLE001
pass
if keywords:
try:
result.ocr_mentions, result.ocr_mention_counts = count_ocr_mentions(
video_path, keywords
)
except Exception: # noqa: BLE001
pass
# --- DEBUG: dummy result for the no-analysis "none" mode (delete to remove) -- #
def _dummy_result() -> AnalysisResult:
"""Fabricated result so the UI can be exercised with zero fal/ML calls."""
from .config import VIDEO_FPS
fps, duration = float(VIDEO_FPS), 10.0
def track(name: str, frames, conf: float) -> ProductResult:
boxes = {
str(f): [Box(x=0.2 + 0.01 * (f % 10), y=0.3, w=0.25, h=0.3,
scores={"similarity": conf})]
for f in frames
}
return build_product_result(name, boxes, fps, duration)
return AnalysisResult(
fps=fps, duration_sec=duration, mode=DetectionMode.none,
products=[
track("debug product A", range(0, 7), 0.92),
track("debug product B", list(range(12, 19)) + list(range(24, 28)), 0.74),
],
brand_name="debug product A, debug product B",
transcript="(debug) no transcription was run.",
audio_mentions=3, caption_mentions=1, ocr_mentions=2,
audio_mention_counts={"debug": 3},
caption_mention_counts={"debug": 1},
ocr_mention_counts={"debug": 2},
)