unknownfriend00007 commited on
Commit
095eb34
·
verified ·
1 Parent(s): 5d5eb19

Upload 16 files

Browse files
video_library/__main__.py CHANGED
@@ -27,23 +27,45 @@ GEMINI_RPD = 500
27
  GEMMA_RPD = 1500
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def default_components():
31
  """Build the real provider, Neon store, and vision router from config/env."""
32
  from .stock_search import PexelsProvider
33
  from .catalog_store import NeonCatalogStore
34
- from .vision_describe import GenAIBackend, ModelConfig, VisionRouter
35
 
36
  store = NeonCatalogStore(config.NEON_DATABASE_URL)
37
  store.ensure_schema()
38
  provider = PexelsProvider(config.PEXELS_API_KEY)
39
- configs = [
40
- ModelConfig(config.GEMINI_MODEL, strip_audio=False, rpd=GEMINI_RPD),
41
- ModelConfig(config.GEMMA_MODEL, strip_audio=True, rpd=GEMMA_RPD),
42
- ]
43
  router = VisionRouter(
44
- configs, store,
45
  backend_factory=lambda cfg: GenAIBackend(
46
- config.GEMINI_API_KEY, cfg.model_id, strip_audio=cfg.strip_audio),
 
47
  today=_utc_date,
48
  )
49
  return provider, store, router
 
27
  GEMMA_RPD = 1500
28
 
29
 
30
+ def _build_model_configs() -> list:
31
+ """Build the fallback chain: Gemini key#1 -> Gemini key#2 -> Gemma key#1 -> Gemma key#2.
32
+
33
+ The bare tracking_id (no suffix) for key #1 keeps existing model_usage rows
34
+ valid after the upgrade. Key #2 entries are only added if GEMINI_API_KEY_2 is set.
35
+ """
36
+ from .vision_describe import ModelConfig
37
+
38
+ configs = [ModelConfig(config.GEMINI_MODEL, strip_audio=False, rpd=GEMINI_RPD,
39
+ api_key=config.GEMINI_API_KEY)]
40
+ if config.GEMINI_API_KEY_2:
41
+ configs.append(ModelConfig(
42
+ config.GEMINI_MODEL, strip_audio=False, rpd=GEMINI_RPD,
43
+ api_key=config.GEMINI_API_KEY_2,
44
+ tracking_id=f"{config.GEMINI_MODEL}#2"))
45
+ configs.append(ModelConfig(config.GEMMA_MODEL, strip_audio=True, rpd=GEMMA_RPD,
46
+ api_key=config.GEMINI_API_KEY))
47
+ if config.GEMINI_API_KEY_2:
48
+ configs.append(ModelConfig(
49
+ config.GEMMA_MODEL, strip_audio=True, rpd=GEMMA_RPD,
50
+ api_key=config.GEMINI_API_KEY_2,
51
+ tracking_id=f"{config.GEMMA_MODEL}#2"))
52
+ return configs
53
+
54
+
55
  def default_components():
56
  """Build the real provider, Neon store, and vision router from config/env."""
57
  from .stock_search import PexelsProvider
58
  from .catalog_store import NeonCatalogStore
59
+ from .vision_describe import GenAIBackend, VisionRouter
60
 
61
  store = NeonCatalogStore(config.NEON_DATABASE_URL)
62
  store.ensure_schema()
63
  provider = PexelsProvider(config.PEXELS_API_KEY)
 
 
 
 
64
  router = VisionRouter(
65
+ _build_model_configs(), store,
66
  backend_factory=lambda cfg: GenAIBackend(
67
+ cfg.api_key or config.GEMINI_API_KEY, cfg.model_id,
68
+ strip_audio=cfg.strip_audio),
69
  today=_utc_date,
70
  )
71
  return provider, store, router
video_library/config.py CHANGED
@@ -35,6 +35,7 @@ _load_dotenv()
35
  # Secrets (env only)
36
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "")
37
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") # AI Studio key (Gemini + Gemma)
 
38
  NEON_DATABASE_URL = os.environ.get("NEON_DATABASE_URL", "") # Neon Postgres DSN
39
 
40
  # Vision model ids (env-overridable; defaults are confirmed AI Studio strings).
 
35
  # Secrets (env only)
36
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY", "")
37
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") # AI Studio key (Gemini + Gemma)
38
+ GEMINI_API_KEY_2 = os.environ.get("GEMINI_API_KEY_2", "") # optional: second key doubles RPD ceiling
39
  NEON_DATABASE_URL = os.environ.get("NEON_DATABASE_URL", "") # Neon Postgres DSN
40
 
41
  # Vision model ids (env-overridable; defaults are confirmed AI Studio strings).
video_library/taxonomy.py CHANGED
@@ -1,10 +1,21 @@
1
  """Mood -> symbolic search queries. Drives SEARCH only; mood assignment is Phase 2.
2
 
3
  Queries are deliberately symbolic, never literal/morbid: 'death' fetches dry
4
- leaves and guttering candles, never corpses or graveyards.
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  MOOD_QUERIES: dict[str, list[str]] = {
 
8
  "silence": [
9
  "mist forest slow motion", "still lake fog", "candle flame dark room",
10
  "empty temple morning", "quiet mountain clouds", "incense smoke close up",
@@ -13,10 +24,6 @@ MOOD_QUERIES: dict[str, list[str]] = {
13
  "meditation silhouette", "monk walking", "incense smoke",
14
  "temple interior", "prayer beads close up", "sunrise mountains calm",
15
  ],
16
- "mind": [
17
- "crowd walking blur", "traffic night timelapse", "clock close up dark",
18
- "shadow hallway", "city lights abstract", "rain window night",
19
- ],
20
  "death": [
21
  "dry leaves slow motion", "river dusk", "candle extinguish",
22
  "old tree fog", "ashes wind", "bare branches winter",
@@ -29,9 +36,56 @@ MOOD_QUERIES: dict[str, list[str]] = {
29
  "warm sunset hands", "flowers sunlight close up", "soft window light",
30
  "birds flying sunset", "gentle silhouette sunset", "warm candle room",
31
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
- INITIAL_MOODS: list[str] = ["silence", "meditation", "mind", "death", "cosmic", "love"]
 
 
 
35
 
36
 
37
  def queries_for(moods: list[str]) -> list[tuple[str, str]]:
 
1
  """Mood -> symbolic search queries. Drives SEARCH only; mood assignment is Phase 2.
2
 
3
  Queries are deliberately symbolic, never literal/morbid: 'death' fetches dry
4
+ leaves and guttering candles, never corpses; 'innocence' fetches bubbles and
5
+ dandelion seeds, not children's faces (which trip Pexels' people/text rejects
6
+ anyway). All queries lean toward abstract motion and natural imagery, which
7
+ Pexels returns cleaner stock for than literal urban/commercial terms.
8
+
9
+ After the first 36-query run on 2026-06-20 we saw 88% approval EXCEPT 'mind'
10
+ (44% rejection) - the literal urban terms surfaced watermarked, text-overlaid
11
+ commercial stock. Mind queries are now rewritten as abstract motion.
12
+
13
+ Pattern: short symbolic noun phrase + modifier like "slow motion", "close up",
14
+ "abstract", "silhouette". These yield cinematic, watermark-free stock.
15
  """
16
 
17
  MOOD_QUERIES: dict[str, list[str]] = {
18
+ # --- Original five (proven ~96-98% approval) ---
19
  "silence": [
20
  "mist forest slow motion", "still lake fog", "candle flame dark room",
21
  "empty temple morning", "quiet mountain clouds", "incense smoke close up",
 
24
  "meditation silhouette", "monk walking", "incense smoke",
25
  "temple interior", "prayer beads close up", "sunrise mountains calm",
26
  ],
 
 
 
 
27
  "death": [
28
  "dry leaves slow motion", "river dusk", "candle extinguish",
29
  "old tree fog", "ashes wind", "bare branches winter",
 
36
  "warm sunset hands", "flowers sunlight close up", "soft window light",
37
  "birds flying sunset", "gentle silhouette sunset", "warm candle room",
38
  ],
39
+
40
+ # --- Rewritten 'mind' (literal urban -> abstract restlessness) ---
41
+ "mind": [
42
+ "crowd walking blur slow motion", "abstract motion blur dark",
43
+ "swirling smoke dark background", "rippling water reflection night",
44
+ "flickering shadows wall", "spinning bokeh lights dark",
45
+ ],
46
+
47
+ # --- New: core Osho metaphors that were missing ---
48
+ "water": [
49
+ "waterfall slow motion mist", "river flowing rocks slow motion",
50
+ "ocean waves close up slow motion", "raindrops puddle slow motion",
51
+ "stream forest sunlight", "waves crashing slow motion",
52
+ ],
53
+ "fire": [
54
+ "bonfire flames close up night", "sparks rising slow motion dark",
55
+ "hearth fire slow motion", "ember glowing close up",
56
+ "candle flame flicker macro", "fire embers wind night",
57
+ ],
58
+ "innocence": [
59
+ "bubble floating sunlight slow motion", "butterfly flower close up slow motion",
60
+ "feather floating slow motion", "dandelion seeds wind slow motion",
61
+ "petals falling slow motion", "kite flying sky slow motion",
62
+ ],
63
+ "devotion": [
64
+ "praying hands close up", "temple bell ring slow motion",
65
+ "candle offering close up", "flower offering hands",
66
+ "incense burning altar", "bowing silhouette sunset",
67
+ ],
68
+ "awakening": [
69
+ "sunrise mountains timelapse", "flower blooming timelapse",
70
+ "dawn fog forest", "first light window slow motion",
71
+ "mist rising valley dawn", "lotus blooming timelapse",
72
+ ],
73
+ "seeking": [
74
+ "footsteps sand slow motion", "door opening light",
75
+ "forest path walking", "mountain trail fog",
76
+ "silhouette walking horizon sunset", "footprints snow slow motion",
77
+ ],
78
+ "body": [
79
+ "silhouette dancing sunset", "whirling dervish silhouette",
80
+ "hands movement slow motion", "contemporary dance silhouette",
81
+ "flowing fabric wind slow motion", "yoga silhouette sunrise",
82
+ ],
83
  }
84
 
85
+ INITIAL_MOODS: list[str] = [
86
+ "silence", "meditation", "mind", "death", "cosmic", "love",
87
+ "water", "fire", "innocence", "devotion", "awakening", "seeking", "body",
88
+ ]
89
 
90
 
91
  def queries_for(moods: list[str]) -> list[tuple[str, str]]:
video_library/vision_describe.py CHANGED
@@ -184,6 +184,12 @@ class ModelConfig:
184
  strip_audio: bool = False
185
  rpd: int = 500 # requests per day (daily cap)
186
  rpm: int = 15 # requests per minute
 
 
 
 
 
 
187
 
188
 
189
  class NoModelAvailableError(Exception):
@@ -214,17 +220,17 @@ class VisionRouter:
214
  self._recent: dict[str, list[float]] = {}
215
 
216
  def backend_for(self, cfg: ModelConfig) -> VisionBackend:
217
- if cfg.model_id not in self._backends:
218
- self._backends[cfg.model_id] = self._backend_factory(cfg)
219
- return self._backends[cfg.model_id]
220
 
221
- def mark_exhausted(self, model_id: str) -> None:
222
- self._exhausted.add(model_id)
223
 
224
  def _enforce_rpm(self, cfg: ModelConfig) -> None:
225
  now = self.monotonic()
226
- window = [t for t in self._recent.get(cfg.model_id, []) if now - t < 60.0]
227
- self._recent[cfg.model_id] = window
228
  if len(window) >= cfg.rpm:
229
  wait = 60.0 - (now - window[0])
230
  if wait > 0:
@@ -233,17 +239,17 @@ class VisionRouter:
233
  def pick_model(self) -> ModelConfig | None:
234
  date = self.today()
235
  for cfg in self.configs:
236
- if cfg.model_id in self._exhausted:
237
  continue
238
- if self.usage_store.get_usage(cfg.model_id, date) >= cfg.rpd:
239
  continue
240
  self._enforce_rpm(cfg)
241
  return cfg
242
  return None
243
 
244
  def note_request(self, cfg: ModelConfig) -> None:
245
- self.usage_store.note_request(cfg.model_id, self.today())
246
- self._recent.setdefault(cfg.model_id, []).append(self.monotonic())
247
 
248
  def describe(self, video_path: str) -> Description:
249
  while True:
@@ -255,7 +261,7 @@ class VisionRouter:
255
  desc = backend.describe(video_path)
256
  except Exception as e:
257
  if is_quota_error(e):
258
- self.mark_exhausted(cfg.model_id)
259
  continue
260
  raise
261
  self.note_request(cfg)
 
184
  strip_audio: bool = False
185
  rpd: int = 500 # requests per day (daily cap)
186
  rpm: int = 15 # requests per minute
187
+ api_key: str = "" # if set, the backend factory uses this instead of a default
188
+ tracking_id: str = "" # usage-store key; defaults to model_id (so single-key setups stay backward-compat)
189
+
190
+ def __post_init__(self):
191
+ if not self.tracking_id:
192
+ self.tracking_id = self.model_id
193
 
194
 
195
  class NoModelAvailableError(Exception):
 
220
  self._recent: dict[str, list[float]] = {}
221
 
222
  def backend_for(self, cfg: ModelConfig) -> VisionBackend:
223
+ if cfg.tracking_id not in self._backends:
224
+ self._backends[cfg.tracking_id] = self._backend_factory(cfg)
225
+ return self._backends[cfg.tracking_id]
226
 
227
+ def mark_exhausted(self, tracking_id: str) -> None:
228
+ self._exhausted.add(tracking_id)
229
 
230
  def _enforce_rpm(self, cfg: ModelConfig) -> None:
231
  now = self.monotonic()
232
+ window = [t for t in self._recent.get(cfg.tracking_id, []) if now - t < 60.0]
233
+ self._recent[cfg.tracking_id] = window
234
  if len(window) >= cfg.rpm:
235
  wait = 60.0 - (now - window[0])
236
  if wait > 0:
 
239
  def pick_model(self) -> ModelConfig | None:
240
  date = self.today()
241
  for cfg in self.configs:
242
+ if cfg.tracking_id in self._exhausted:
243
  continue
244
+ if self.usage_store.get_usage(cfg.tracking_id, date) >= cfg.rpd:
245
  continue
246
  self._enforce_rpm(cfg)
247
  return cfg
248
  return None
249
 
250
  def note_request(self, cfg: ModelConfig) -> None:
251
+ self.usage_store.note_request(cfg.tracking_id, self.today())
252
+ self._recent.setdefault(cfg.tracking_id, []).append(self.monotonic())
253
 
254
  def describe(self, video_path: str) -> Description:
255
  while True:
 
261
  desc = backend.describe(video_path)
262
  except Exception as e:
263
  if is_quota_error(e):
264
+ self.mark_exhausted(cfg.tracking_id)
265
  continue
266
  raise
267
  self.note_request(cfg)