File size: 6,332 Bytes
d1430bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169b1a0
d1430bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169b1a0
d1430bc
169b1a0
 
 
d1430bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Public-safe associated video coverage for eligible representative batches.

Every active member contributes its full measured duration to the bucket of its
collection start. These durations describe associated video coverage, without
asserting that a representative frame's labels persist throughout the videos.
"""
from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
import math
import re

import psycopg

from annotation_statistics import validate_public_labels
from live_inventory import _period_for_hour, _valid_number


_ELIGIBLE_BATCH_SQL = """
SELECT b.batch_id, b.labels, b.schema_version
FROM aerial.annotation_batches b
JOIN aerial.videos representative
  ON representative.sha256 = b.representative_video_sha256
WHERE b.processing_info->>'status' = 'completed'
  AND b.validation_passed IS TRUE
  AND b.review_status IS DISTINCT FROM 'rejected'
  AND representative.deleted_at IS NULL
  AND EXISTS (
      SELECT 1 FROM aerial.annotation_batch_videos bv
      WHERE bv.batch_id = b.batch_id
        AND bv.video_sha256 = representative.sha256
  )
ORDER BY b.batch_id
"""
_MEMBER_SQL = """
SELECT bv.batch_id, v.sha256,
       (v.collected_at AT TIME ZONE 'Asia/Shanghai')::date,
       EXTRACT(HOUR FROM v.collected_at AT TIME ZONE 'Asia/Shanghai')::integer,
       v.codec, v.duration_seconds
FROM aerial.annotation_batch_videos bv
JOIN aerial.videos v ON v.sha256 = bv.video_sha256
WHERE bv.batch_id = ANY(%s)
  AND v.deleted_at IS NULL
ORDER BY bv.batch_id, v.sha256
"""
_PUBLIC_CODE = re.compile(r'B-[0-9a-f]{16}')


@dataclass(frozen=True)
class CoverageSlice:
    collected_on: date | None
    period: str
    codec: str
    known_seconds: float
    known_videos: int
    unknown_videos: int


@dataclass(frozen=True)
class BatchCoverage:
    batch_code: str
    slices: tuple[CoverageSlice, ...]


@dataclass(frozen=True)
class CoverageCatalog:
    records: tuple[BatchCoverage, ...]


def _slice_key(item: CoverageSlice) -> tuple[bool, date, str, str]:
    return item.collected_on is None, item.collected_on or date.max, item.period, item.codec


def _validated_code(value: object) -> str:
    if not isinstance(value, str) or not _PUBLIC_CODE.fullmatch(value):
        raise ValueError('Invalid coverage batch code')
    return value


def align_coverage_catalog(catalog: CoverageCatalog, batch_codes: Iterable[str]) -> CoverageCatalog:
    """Match independent source coverage to the exact exported batch population.

    Empty slices mark unavailable member coverage; a known zero remains a slice
    with a positive known-video count. Reject duplicate source or export codes
    rather than silently hiding an inconsistent publication population.
    """
    records = {}
    for record in catalog.records:
        code = _validated_code(record.batch_code)
        if code in records:
            raise ValueError('Duplicate coverage batch code')
        records[code] = record
    selected = set()
    for value in batch_codes:
        code = _validated_code(value)
        if code in selected:
            raise ValueError('Duplicate coverage batch code')
        selected.add(code)
    return CoverageCatalog(tuple(
        BatchCoverage(code, tuple(sorted(records[code].slices, key=_slice_key)))
        if code in records else BatchCoverage(code, ())
        for code in sorted(selected)
    ))


def fetch_coverage_catalog(dsn: str) -> CoverageCatalog:
    """Read eligible labels once per batch, then only active member metadata.

    Private identifiers are used locally for membership checks and discarded.
    Duplicate membership rows inside a batch are counted once. Conflicting
    active memberships across eligible batches fail with an identifier-free
    error. Missing, negative and nonfinite durations count as unknown videos;
    observed zero counts as a known duration. No duration is inferred.
    """
    # batch_updates imports the label validator too; keep a single safe B-code
    # contract without creating an import-time dependency cycle.
    from batch_updates import _public_code

    with psycopg.connect(dsn, options='-c default_transaction_read_only=on') as connection:
        with connection.cursor() as cursor:
            cursor.execute(_ELIGIBLE_BATCH_SQL)
            eligible = []
            for batch_id, raw_labels, schema_version in cursor.fetchall():
                try:
                    if schema_version not in ('aerial_annotation_v1', 'aerial_annotation_v2'):
                        raise ValueError('Unsupported source annotation schema')
                    validate_public_labels(raw_labels, schema_version)
                except ValueError:
                    continue
                eligible.append(batch_id)
            if not eligible:
                return CoverageCatalog(())
            cursor.execute(_MEMBER_SQL, (eligible,))
            rows = cursor.fetchall()

    buckets = {batch_id: {} for batch_id in eligible}
    ownership = {}
    for batch_id, video_id, collected_on, hour, codec, seconds in rows:
        if video_id in ownership:
            if ownership[video_id] != batch_id:
                raise ValueError('Conflicting active video batch membership')
            continue
        ownership[video_id] = batch_id
        key = (collected_on, _period_for_hour(hour),
               codec.strip() if isinstance(codec, str) and codec.strip() else '未知')
        bucket = buckets[batch_id].setdefault(key, [Decimal(0), 0, 0])
        duration = _valid_number(seconds)
        if duration is None:
            bucket[2] += 1
        else:
            bucket[0] += duration
            bucket[1] += 1

    records = []
    for batch_id, batch_buckets in buckets.items():
        slices = []
        for (collected_on, period, codec), (seconds, known, unknown) in batch_buckets.items():
            total = float(seconds)
            if not math.isfinite(total):
                raise ValueError('Invalid coverage duration total')
            slices.append(CoverageSlice(collected_on, period, codec, total, known, unknown))
        records.append(BatchCoverage(_public_code(batch_id), tuple(sorted(slices, key=_slice_key))))
    return CoverageCatalog(tuple(sorted(records, key=lambda record: record.batch_code)))