caption-flow 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
caption_flow/__init__.py CHANGED
@@ -3,7 +3,8 @@
3
3
  __version__ = "0.1.0"
4
4
 
5
5
  from .orchestrator import Orchestrator
6
- from .worker import Worker
6
+ from .workers.data import DataWorker
7
+ from .workers.caption import CaptionWorker
7
8
  from .monitor import Monitor
8
9
 
9
- __all__ = ["Orchestrator", "Worker", "Monitor"]
10
+ __all__ = ["Orchestrator", "DataWorker", "CaptionWorker", "Monitor"]
caption_flow/cli.py CHANGED
@@ -15,7 +15,6 @@ from rich.logging import RichHandler
15
15
  from datetime import datetime
16
16
 
17
17
  from .orchestrator import Orchestrator
18
- from .worker import Worker
19
18
  from .monitor import Monitor
20
19
  from .utils.certificates import CertificateManager
21
20
 
@@ -121,13 +120,19 @@ class ConfigManager:
121
120
 
122
121
 
123
122
  def setup_logging(verbose: bool = False):
124
- """Configure logging with rich handler."""
123
+ """Configure logging with rich handler, including timestamp."""
125
124
  level = logging.DEBUG if verbose else logging.INFO
126
125
  logging.basicConfig(
127
126
  level=level,
128
- format="%(message)s",
127
+ format="%(asctime)s %(message)s",
128
+ datefmt="[%Y-%m-%d %H:%M:%S]",
129
129
  handlers=[
130
- RichHandler(console=console, rich_tracebacks=True, show_path=False, show_time=False)
130
+ RichHandler(
131
+ console=console,
132
+ rich_tracebacks=True,
133
+ show_path=False,
134
+ show_time=True, # Enables timestamp in RichHandler output
135
+ )
131
136
  ],
132
137
  )
133
138
 
@@ -168,8 +173,6 @@ def orchestrator(ctx, config: Optional[str], **kwargs):
168
173
  else:
169
174
  config_data = base_config
170
175
 
171
- console.print(f"Config contents: {config_data}")
172
-
173
176
  # Apply CLI overrides
174
177
  if kwargs.get("port"):
175
178
  config_data["port"] = kwargs["port"]
@@ -242,11 +245,11 @@ def worker(ctx, config: Optional[str], **kwargs):
242
245
 
243
246
  # Choose worker type
244
247
  if kwargs.get("vllm") or config_data.get("vllm"):
245
- from .worker_vllm import VLLMWorker
248
+ from .workers.caption import CaptionWorker
246
249
 
247
- worker_instance = VLLMWorker(config_data)
250
+ worker_instance = CaptionWorker(config_data)
248
251
  else:
249
- worker_instance = Worker(config_data)
252
+ raise ValueError(f"Not sure how to handle worker for {config_data.get('type')} type setup.")
250
253
 
251
254
  try:
252
255
  asyncio.run(worker_instance.start())
@@ -262,84 +265,92 @@ def worker(ctx, config: Optional[str], **kwargs):
262
265
  @click.option("--no-verify-ssl", is_flag=True, help="Skip SSL verification")
263
266
  @click.option("--debug", is_flag=True, help="Enable debug output")
264
267
  @click.pass_context
265
- def monitor(ctx, config: Optional[str], server: Optional[str], token: Optional[str],
266
- no_verify_ssl: bool, debug: bool):
268
+ def monitor(
269
+ ctx,
270
+ config: Optional[str],
271
+ server: Optional[str],
272
+ token: Optional[str],
273
+ no_verify_ssl: bool,
274
+ debug: bool,
275
+ ):
267
276
  """Start the monitoring TUI."""
268
-
277
+
269
278
  # Enable debug logging if requested
270
279
  if debug:
271
280
  setup_logging(verbose=True)
272
281
  console.print("[yellow]Debug mode enabled[/yellow]")
273
-
282
+
274
283
  # Load configuration
275
- base_config = ConfigManager.find_config('monitor', config)
276
-
284
+ base_config = ConfigManager.find_config("monitor", config)
285
+
277
286
  if not base_config:
278
287
  # Try to find monitor config in orchestrator config as fallback
279
- orch_config = ConfigManager.find_config('orchestrator')
280
- if orch_config and 'monitor' in orch_config:
281
- base_config = {'monitor': orch_config['monitor']}
288
+ orch_config = ConfigManager.find_config("orchestrator")
289
+ if orch_config and "monitor" in orch_config:
290
+ base_config = {"monitor": orch_config["monitor"]}
282
291
  console.print("[dim]Using monitor config from orchestrator.yaml[/dim]")
283
292
  else:
284
293
  base_config = {}
285
294
  if not server or not token:
286
295
  console.print("[yellow]No monitor config found, using CLI args[/yellow]")
287
-
296
+
288
297
  # Handle different config structures
289
298
  # Case 1: Config has top-level 'monitor' section
290
- if 'monitor' in base_config:
291
- config_data = base_config['monitor']
299
+ if "monitor" in base_config:
300
+ config_data = base_config["monitor"]
292
301
  # Case 2: Config IS the monitor config (no wrapper)
293
302
  else:
294
303
  config_data = base_config
295
-
304
+
296
305
  # Apply CLI overrides (CLI always wins)
297
306
  if server:
298
- config_data['server'] = server
307
+ config_data["server"] = server
299
308
  if token:
300
- config_data['token'] = token
309
+ config_data["token"] = token
301
310
  if no_verify_ssl:
302
- config_data['verify_ssl'] = False
303
-
311
+ config_data["verify_ssl"] = False
312
+
304
313
  # Debug output
305
314
  if debug:
306
315
  console.print("\n[cyan]Final monitor configuration:[/cyan]")
307
316
  console.print(f" Server: {config_data.get('server', 'NOT SET')}")
308
- console.print(f" Token: {'***' + config_data.get('token', '')[-4:] if config_data.get('token') else 'NOT SET'}")
317
+ console.print(
318
+ f" Token: {'***' + config_data.get('token', '')[-4:] if config_data.get('token') else 'NOT SET'}"
319
+ )
309
320
  console.print(f" Verify SSL: {config_data.get('verify_ssl', True)}")
310
321
  console.print()
311
-
322
+
312
323
  # Validate required fields
313
- if not config_data.get('server'):
324
+ if not config_data.get("server"):
314
325
  console.print("[red]Error: --server required (or set 'server' in monitor.yaml)[/red]")
315
326
  console.print("\n[dim]Example monitor.yaml:[/dim]")
316
327
  console.print("server: wss://localhost:8765")
317
328
  console.print("token: your-token-here")
318
329
  sys.exit(1)
319
-
320
- if not config_data.get('token'):
330
+
331
+ if not config_data.get("token"):
321
332
  console.print("[red]Error: --token required (or set 'token' in monitor.yaml)[/red]")
322
333
  console.print("\n[dim]Example monitor.yaml:[/dim]")
323
334
  console.print("server: wss://localhost:8765")
324
335
  console.print("token: your-token-here")
325
336
  sys.exit(1)
326
-
337
+
327
338
  # Set defaults for optional settings
328
- config_data.setdefault('refresh_interval', 1.0)
329
- config_data.setdefault('show_inactive_workers', False)
330
- config_data.setdefault('max_log_lines', 100)
331
-
339
+ config_data.setdefault("refresh_interval", 1.0)
340
+ config_data.setdefault("show_inactive_workers", False)
341
+ config_data.setdefault("max_log_lines", 100)
342
+
332
343
  # Create and start monitor
333
344
  try:
334
345
  monitor_instance = Monitor(config_data)
335
-
346
+
336
347
  if debug:
337
348
  console.print("[green]Starting monitor...[/green]")
338
349
  console.print(f"[dim]Connecting to: {config_data['server']}[/dim]")
339
350
  sys.exit(1)
340
-
351
+
341
352
  asyncio.run(monitor_instance.start())
342
-
353
+
343
354
  except KeyboardInterrupt:
344
355
  console.print("\n[yellow]Closing monitor...[/yellow]")
345
356
  except ConnectionRefusedError:
@@ -350,9 +361,11 @@ def monitor(ctx, config: Optional[str], server: Optional[str], token: Optional[s
350
361
  console.print(f"\n[red]Error starting monitor: {e}[/red]")
351
362
  if debug:
352
363
  import traceback
364
+
353
365
  traceback.print_exc()
354
366
  sys.exit(1)
355
367
 
368
+
356
369
  @main.command()
357
370
  @click.option("--config", type=click.Path(exists=True), help="Configuration file")
358
371
  @click.option("--server", help="Orchestrator WebSocket URL")
@@ -376,11 +389,21 @@ def reload_config(
376
389
  if not server or not token:
377
390
  base_config = ConfigManager.find_config("orchestrator", config) or {}
378
391
  admin_config = base_config.get("admin", {})
392
+ admin_tokens = base_config.get("orchestrator", {}).get("auth", {}).get("admin_tokens", [])
393
+ has_admin_tokens = False
394
+ if len(admin_tokens) > 0:
395
+ has_admin_tokens = True
396
+ first_admin_token = admin_tokens[0].get("token", None)
397
+ # Do not print sensitive admin token to console.
379
398
 
380
399
  if not server:
381
- server = admin_config.get("server")
400
+ server = admin_config.get("server", "ws://localhost:8765")
382
401
  if not token:
383
- token = admin_config.get("token")
402
+ token = admin_config.get("token", None)
403
+ if token is None and has_admin_tokens:
404
+ # grab the first one, we'll just assume we're localhost.
405
+ console.print("Using first admin token.")
406
+ token = first_admin_token
384
407
 
385
408
  if not server:
386
409
  console.print("[red]Error: --server required (or set in config)[/red]")
@@ -447,7 +470,7 @@ def reload_config(
447
470
  return True
448
471
  else:
449
472
  error = reload_response.get("error", "Unknown error")
450
- console.print(f"[red]Reload failed: {error}[/red]")
473
+ console.print(f"[red]Reload failed: {error} ({reload_response=})[/red]")
451
474
  return False
452
475
 
453
476
  except Exception as e:
caption_flow/models.py CHANGED
@@ -1,9 +1,9 @@
1
1
  """Data models for CaptionFlow."""
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from datetime import datetime
5
5
  from enum import Enum
6
- from typing import Optional
6
+ from typing import Any, Dict, List, Optional
7
7
 
8
8
 
9
9
  class JobStatus(Enum):
@@ -51,9 +51,10 @@ class Caption:
51
51
  timestamp: datetime
52
52
  caption_count: int = 1 # Number of captions generated for this item
53
53
  caption: Optional[str] = None
54
- captions: Optional[list] = None
54
+ captions: Optional[List[str]] = None
55
+ outputs: Dict[str, List[str]] = field(default_factory=dict)
55
56
  quality_score: Optional[float] = None
56
- quality_scores: Optional[list] = None
57
+ quality_scores: Optional[List[float]] = None
57
58
 
58
59
  # Image metadata
59
60
  image_width: Optional[int] = None
@@ -66,6 +67,7 @@ class Caption:
66
67
  total_captions: Optional[int] = None # Total captions for this image
67
68
  processing_time_ms: Optional[float] = None
68
69
  chunk_id: Optional[str] = None
70
+ metadata: Dict[str, Any] = field(default_factory=dict)
69
71
 
70
72
  def __post_init__(self):
71
73
  if self.caption is None and self.captions is None:
caption_flow/monitor.py CHANGED
@@ -107,7 +107,7 @@ class Monitor:
107
107
  """Main display update loop."""
108
108
  layout = self._create_layout()
109
109
 
110
- with Live(layout, console=self.console, refresh_per_second=4, screen=True) as live:
110
+ with Live(layout, console=self.console, refresh_per_second=1, screen=True) as live:
111
111
  while self.running:
112
112
  self._update_layout(layout)
113
113
  await asyncio.sleep(0.25)
@@ -166,6 +166,8 @@ class Monitor:
166
166
  # Filter out rate stats (already shown in rates panel)
167
167
  for key, value in self.stats.items():
168
168
  if key not in ["current_rate", "average_rate", "expected_rate"]:
169
+ if isinstance(value, dict):
170
+ value = json.dumps(value, indent=2)
169
171
  stats_table.add_row(key.replace("_", " ").title(), str(value))
170
172
 
171
173
  layout["stats"].update(Panel(stats_table, title="System Statistics", border_style="green"))
@@ -178,9 +180,18 @@ class Monitor:
178
180
  leaderboard_table.add_column("Trust", style="green")
179
181
 
180
182
  for i, contributor in enumerate(self.leaderboard[:10], 1):
183
+ # Format name with active worker count
184
+ name = contributor.get("name", "Unknown")
185
+ active_workers = contributor.get("active_workers", 0)
186
+
187
+ if active_workers > 0:
188
+ name_display = f"{name} [bright_green](x{active_workers})[/bright_green]"
189
+ else:
190
+ name_display = f"{name} [dim](offline)[/dim]"
191
+
181
192
  leaderboard_table.add_row(
182
193
  str(i),
183
- contributor.get("name", "Unknown"),
194
+ name_display,
184
195
  str(contributor.get("total_captions", 0)),
185
196
  "⭐" * contributor.get("trust_level", 0),
186
197
  )
@@ -188,7 +199,6 @@ class Monitor:
188
199
  layout["leaderboard"].update(
189
200
  Panel(leaderboard_table, title="Top Contributors", border_style="yellow")
190
201
  )
191
-
192
202
  # Activity panel
193
203
  activity_text = Text()
194
204
  for activity in self.recent_activity[-10:]: