Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.9.5
4.9.6
2 changes: 1 addition & 1 deletion gas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "4.9.5"
__version__ = "4.9.6"

version_split = __version__.split(".")
__spec_version__ = (
Expand Down
13 changes: 10 additions & 3 deletions gas/cache/content_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,19 @@ def write_miner_media(

resolution, file_size = extract_media_info(save_path, modality)

# Audio presence feeds resolution/audio-tiered reward pricing.
# Audio presence and duration feed the tiered reward pricing.
has_audio = None
duration_seconds = None
if modality == Modality.VIDEO:
try:
from gas.cache.util.video import get_video_metadata

has_audio = bool(get_video_metadata(save_path).get("has_audio"))
video_meta = get_video_metadata(save_path)
has_audio = bool(video_meta.get("has_audio"))
raw_duration = video_meta.get("duration")
duration_seconds = float(raw_duration) if raw_duration else None
except Exception as e:
bt.logging.warning(f"Could not detect audio track in {save_path}: {e}")
bt.logging.warning(f"Could not probe video metadata in {save_path}: {e}")

media_id = self.media.add_media_entry(
prompt_id=prompt_id,
Expand All @@ -246,6 +250,7 @@ def write_miner_media(
c2pa_issuer=c2pa_issuer,
task_id=task_id,
has_audio=has_audio,
duration_seconds=duration_seconds,
)
self.challenges.update_outcome(
task_id=task_id,
Expand Down Expand Up @@ -624,6 +629,7 @@ def record_challenge_outcome(
media_id: Optional[str] = None,
created_at: Optional[float] = None,
requested_resolution: Optional[str] = None,
requested_duration: Optional[float] = None,
) -> bool:
return self.challenges.record_outcome(
task_id=task_id,
Expand All @@ -636,6 +642,7 @@ def record_challenge_outcome(
media_id=media_id,
created_at=created_at,
requested_resolution=requested_resolution,
requested_duration=requested_duration,
)

def update_challenge_outcome(
Expand Down
18 changes: 14 additions & 4 deletions gas/cache/db/challenge_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def record_outcome(
media_id: Optional[str] = None,
created_at: Optional[float] = None,
requested_resolution: Optional[str] = None,
requested_duration: Optional[float] = None,
) -> bool:
"""Insert or update a generation challenge outcome."""
try:
Expand All @@ -54,9 +55,10 @@ def record_outcome(
"""
INSERT INTO generator_challenge_outcomes (
task_id, uid, hotkey, prompt_id, modality, status,
failure_reason, media_id, requested_resolution, created_at, updated_at
failure_reason, media_id, requested_resolution,
requested_duration, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(task_id) DO UPDATE SET
uid = excluded.uid,
hotkey = excluded.hotkey,
Expand All @@ -66,9 +68,10 @@ def record_outcome(
failure_reason = excluded.failure_reason,
media_id = COALESCE(excluded.media_id, generator_challenge_outcomes.media_id),
requested_resolution = COALESCE(excluded.requested_resolution, generator_challenge_outcomes.requested_resolution),
requested_duration = COALESCE(excluded.requested_duration, generator_challenge_outcomes.requested_duration),
updated_at = excluded.updated_at
""",
(task_id, uid, hotkey, prompt_id, modality, status, failure_reason, media_id, requested_resolution, created_at, now),
(task_id, uid, hotkey, prompt_id, modality, status, failure_reason, media_id, requested_resolution, requested_duration, created_at, now),
)
conn.commit()
return True
Expand Down Expand Up @@ -158,7 +161,8 @@ def get_outcomes_last_n_hours(
conn.row_factory = sqlite3.Row
cursor = conn.execute(
"""
SELECT o.*, m.resolution AS media_resolution, m.has_audio AS media_has_audio
SELECT o.*, m.resolution AS media_resolution, m.has_audio AS media_has_audio,
m.duration_seconds AS media_duration
FROM generator_challenge_outcomes o
LEFT JOIN media m ON o.media_id = m.id
WHERE o.status IN ('verified', 'failed') AND o.updated_at >= ?
Expand All @@ -184,6 +188,10 @@ def get_outcomes_last_n_hours(
has_audio=(
bool(row["media_has_audio"]) if row["media_has_audio"] is not None else None
),
requested_duration=(
row["requested_duration"] if "requested_duration" in row.keys() else None
),
observed_duration=row["media_duration"],
created_at=row["created_at"],
updated_at=row["updated_at"],
)
Expand Down Expand Up @@ -239,6 +247,8 @@ def get_outcome_stats_last_n_hours(
if outcome.observed_resolution else None
),
"has_audio": outcome.has_audio,
"requested_duration": outcome.requested_duration,
"observed_duration": outcome.observed_duration,
})
if outcome.media_id:
miner_stats[hotkey]["verified_media_ids"].append(outcome.media_id)
Expand Down
8 changes: 6 additions & 2 deletions gas/cache/db/media_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def _row_to_media_entry(row) -> MediaEntry:
c2pa_verified=bool(row["c2pa_verified"]) if "c2pa_verified" in row.keys() and row["c2pa_verified"] is not None else False,
c2pa_issuer=row["c2pa_issuer"] if "c2pa_issuer" in row.keys() else None,
has_audio=bool(row["has_audio"]) if "has_audio" in row.keys() and row["has_audio"] is not None else None,
duration_seconds=row["duration_seconds"] if "duration_seconds" in row.keys() else None,
)

# ------------------------------------------------------------------
Expand Down Expand Up @@ -127,6 +128,7 @@ def add_media_entry(
c2pa_issuer: Optional[str] = None,
task_id: Optional[str] = None,
has_audio: Optional[bool] = None,
duration_seconds: Optional[float] = None,
) -> str:
"""Add a media entry to the database. Returns the media id."""
media_id = str(uuid.uuid4())
Expand All @@ -141,9 +143,10 @@ def add_media_entry(
model_name, generation_args, uid, hotkey,
verified, failed_verification, rewarded,
created_at, mask_path, timestamp, resolution, file_size, format,
perceptual_hash, c2pa_verified, c2pa_issuer, task_id, has_audio
perceptual_hash, c2pa_verified, c2pa_issuer, task_id, has_audio,
duration_seconds
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
media_id, prompt_id, file_path, modality.value, media_type.value, source_type.value,
Expand All @@ -158,6 +161,7 @@ def add_media_entry(
c2pa_issuer,
task_id,
1 if has_audio else 0 if has_audio is not None else None,
duration_seconds,
),
)
conn.commit()
Expand Down
7 changes: 7 additions & 0 deletions gas/cache/db/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
"CREATE INDEX IF NOT EXISTS idx_prompts_register ON prompts (register)",
],
),
(
"add_duration_columns",
[
"ALTER TABLE generator_challenge_outcomes ADD COLUMN requested_duration REAL",
"ALTER TABLE media ADD COLUMN duration_seconds REAL",
],
),
]


Expand Down
5 changes: 5 additions & 0 deletions gas/cache/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class MediaEntry:
# Video audio track presence (None = unknown / not applicable)
has_audio: Optional[bool] = None

# Video duration in seconds (None = unknown / not applicable)
duration_seconds: Optional[float] = None

# Miner challenge tracking
task_id: Optional[str] = None

Expand Down Expand Up @@ -147,6 +150,8 @@ class ChallengeOutcome:
requested_resolution: Optional[str] = None # tier requested in the challenge (e.g. "1080p")
observed_resolution: Optional[tuple] = None # (width, height) of the stored media
has_audio: Optional[bool] = None # audio track present in the stored media
requested_duration: Optional[float] = None # video seconds requested in the challenge
observed_duration: Optional[float] = None # seconds of the stored media (ffprobe)
created_at: float = None
updated_at: float = None

Expand Down
12 changes: 11 additions & 1 deletion gas/evaluation/generative_challenge_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from typing import Dict, Optional

from gas.cache.content_manager import ContentManager
from gas.evaluation.resolution_tiers import sample_challenge_tier
from gas.evaluation.resolution_tiers import sample_challenge_duration, sample_challenge_tier
from gas.protocol.epistula import get_verifier
from gas.protocol.validator_requests import query_generative_miner
from gas.types import MediaType, MinerType, Modality
Expand Down Expand Up @@ -157,6 +157,14 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali
requested_resolution = sample_challenge_tier(modality.value)
parameters = {"resolution": requested_resolution}

# Video challenges also request a duration; reward pricing scales with
# min(delivered, requested) seconds so shorter deliveries earn less
# rather than failing (mirrors the resolution tier mechanism).
requested_duration = None
if modality == Modality.VIDEO:
requested_duration = sample_challenge_duration()
parameters["duration"] = requested_duration

async with aiohttp.ClientSession() as session:
response_data = await query_generative_miner(
uid=uid,
Expand Down Expand Up @@ -184,6 +192,7 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali
"status": "pending",
"sent_at": time.time(),
"requested_resolution": requested_resolution,
"requested_duration": requested_duration,
}
self.content_manager.record_challenge_outcome(
task_id=miner_task_id,
Expand All @@ -193,6 +202,7 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali
modality=modality.value,
status="pending",
requested_resolution=requested_resolution,
requested_duration=requested_duration,
)
bt.logging.info(
f"Stored challenge task {miner_task_id} for UID {uid}. Total active tasks: {len(self.challenge_tasks)}"
Expand Down
21 changes: 21 additions & 0 deletions gas/evaluation/resolution_tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@
},
}

# Weighted distribution for sampling the requested video duration (seconds).
# All trusted video models accept 4/6/8 (Veo exactly these; Seedance 4-15).
# Miners previously converged on the 4s provider minimum because duration was
# never requested and rewards were duration-invariant; reward pricing now
# scales with min(delivered, requested) seconds (see rewards), so requesting
# longer durations probes real capability and diversifies the dataset.
# Weighted toward 4s to bound miner cost (expected duration ~5.2s).
CHALLENGE_DURATION_WEIGHTS: Dict[int, float] = {
4: 0.50,
6: 0.30,
8: 0.20,
}


def sample_challenge_duration(rng: random.Random = random) -> int:
"""Sample a requested video duration (seconds) from the weight distribution."""
durations = list(CHALLENGE_DURATION_WEIGHTS.keys())
weights = list(CHALLENGE_DURATION_WEIGHTS.values())
return rng.choices(durations, weights=weights, k=1)[0]


# Video classification thresholds on *total pixel count*, which is both
# orientation-invariant and aspect-ratio-invariant. Min-dimension thresholds
# were exploitable via square output: a 640x640 video has the pixel count
Expand Down
Loading
Loading