caption-flow 0.1.0__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 +9 -0
- caption_flow/cli.py +709 -0
- caption_flow/models.py +82 -0
- caption_flow/monitor.py +211 -0
- caption_flow/orchestrator.py +1301 -0
- caption_flow/storage.py +694 -0
- caption_flow/utils/__init__.py +4 -0
- caption_flow/utils/auth.py +67 -0
- caption_flow/utils/caption_utils.py +172 -0
- caption_flow/utils/certificates.py +140 -0
- caption_flow/utils/chunk_tracker.py +365 -0
- caption_flow/utils/dataset_loader.py +186 -0
- caption_flow/utils/image_processor.py +51 -0
- caption_flow/utils/job_queue.py +41 -0
- caption_flow/utils/json_utils.py +201 -0
- caption_flow/utils/vllm_config.py +164 -0
- caption_flow/worker.py +300 -0
- caption_flow/worker_data.py +482 -0
- caption_flow/worker_vllm.py +1028 -0
- caption_flow-0.1.0.dist-info/METADATA +427 -0
- caption_flow-0.1.0.dist-info/RECORD +25 -0
- caption_flow-0.1.0.dist-info/WHEEL +5 -0
- caption_flow-0.1.0.dist-info/entry_points.txt +2 -0
- caption_flow-0.1.0.dist-info/licenses/LICENSE +661 -0
- caption_flow-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Authentication management."""
|
2
|
+
|
3
|
+
from typing import Dict, Any, Optional
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class WorkerAuthenticationDetails:
|
9
|
+
"""Details for worker authentication."""
|
10
|
+
|
11
|
+
name: str
|
12
|
+
token: str
|
13
|
+
role: str
|
14
|
+
|
15
|
+
|
16
|
+
class AuthManager:
|
17
|
+
"""Manages authentication tokens."""
|
18
|
+
|
19
|
+
def __init__(self, config: Dict[str, Any]):
|
20
|
+
self.reload_config(config=config)
|
21
|
+
|
22
|
+
def authenticate(self, token: str) -> Optional[str]:
|
23
|
+
"""Authenticate token and return role."""
|
24
|
+
role = None
|
25
|
+
for worker_token in self.worker_tokens:
|
26
|
+
if token == worker_token:
|
27
|
+
role = "worker"
|
28
|
+
break
|
29
|
+
if role is None:
|
30
|
+
for admin_token in self.admin_tokens:
|
31
|
+
if token == admin_token:
|
32
|
+
role = "admin"
|
33
|
+
break
|
34
|
+
if role is None:
|
35
|
+
for monitor_token in self.monitor_tokens:
|
36
|
+
if token == monitor_token:
|
37
|
+
role = "monitor"
|
38
|
+
break
|
39
|
+
|
40
|
+
worker_auth_details = WorkerAuthenticationDetails(
|
41
|
+
role=role, name=self.worker_tokens.get(token, f"Anonymous {role}"), token=token
|
42
|
+
)
|
43
|
+
return worker_auth_details
|
44
|
+
|
45
|
+
def reload_config(self, config: dict) -> None:
|
46
|
+
"""Reload configuration from file."""
|
47
|
+
self.worker_tokens = {}
|
48
|
+
self.admin_tokens = {}
|
49
|
+
self.monitor_tokens = {}
|
50
|
+
|
51
|
+
# Load worker tokens
|
52
|
+
for worker in config.get("worker_tokens", []):
|
53
|
+
worker_name = worker.get("name", None)
|
54
|
+
assert worker_name is not None, "Worker token must have a name"
|
55
|
+
self.worker_tokens[worker["token"]] = worker_name
|
56
|
+
|
57
|
+
# Load admin tokens
|
58
|
+
for admin in config.get("admin_tokens", []):
|
59
|
+
admin_name = admin.get("name", None)
|
60
|
+
assert admin_name is not None, "Admin token must have a name"
|
61
|
+
self.admin_tokens[admin["token"]] = admin_name
|
62
|
+
|
63
|
+
# Load monitor tokens
|
64
|
+
for monitor in config.get("monitor_tokens", []):
|
65
|
+
monitor_name = monitor.get("name", None)
|
66
|
+
assert monitor_name is not None, "Monitor token must have a name"
|
67
|
+
self.monitor_tokens[monitor["token"]] = monitor_name
|
@@ -0,0 +1,172 @@
|
|
1
|
+
"""Caption processing utilities from the original vLLM script."""
|
2
|
+
|
3
|
+
from typing import List, Dict
|
4
|
+
|
5
|
+
|
6
|
+
class CaptionUtils:
|
7
|
+
"""Utilities for cleaning and combining captions."""
|
8
|
+
|
9
|
+
@staticmethod
|
10
|
+
def clean_caption(c: str) -> str:
|
11
|
+
"""Clean a single caption by removing generic phrases and formatting."""
|
12
|
+
if not c:
|
13
|
+
return ""
|
14
|
+
|
15
|
+
generic = [
|
16
|
+
"in this image we can see ",
|
17
|
+
"this image shows ",
|
18
|
+
"the image depicts ",
|
19
|
+
"the image features ",
|
20
|
+
"this is an image of ",
|
21
|
+
"the image contains ",
|
22
|
+
"the picture shows ",
|
23
|
+
"we can see ",
|
24
|
+
"there is ",
|
25
|
+
"there are ",
|
26
|
+
]
|
27
|
+
|
28
|
+
low = c.lower()
|
29
|
+
for p in generic:
|
30
|
+
if low.startswith(p):
|
31
|
+
c = c[len(p) :]
|
32
|
+
if c:
|
33
|
+
c = c[0].upper() + c[1:]
|
34
|
+
break
|
35
|
+
|
36
|
+
# Remove leading articles if the rest isn't capitalized
|
37
|
+
if c.lower().startswith(("a ", "an ")):
|
38
|
+
parts = c.split(maxsplit=1)
|
39
|
+
if len(parts) > 1 and not parts[1][0].isupper():
|
40
|
+
c = parts[1]
|
41
|
+
c = c[0].upper() + c[1:]
|
42
|
+
|
43
|
+
# Clean whitespace
|
44
|
+
c = " ".join(c.split())
|
45
|
+
|
46
|
+
# Add period if missing
|
47
|
+
if c and c[-1] not in ".!?":
|
48
|
+
c += "."
|
49
|
+
|
50
|
+
return c
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def combine(cls, descs: List[str]) -> str:
|
54
|
+
"""Combine multiple descriptions into a rich, multi-line caption."""
|
55
|
+
if not descs:
|
56
|
+
return ""
|
57
|
+
|
58
|
+
filtered = []
|
59
|
+
heads = [
|
60
|
+
"in this image we can see",
|
61
|
+
"this image shows",
|
62
|
+
"the image depicts",
|
63
|
+
"a cartoon",
|
64
|
+
"a drawing",
|
65
|
+
"an illustration",
|
66
|
+
]
|
67
|
+
|
68
|
+
# Filter out short generic descriptions
|
69
|
+
for d in descs:
|
70
|
+
if not d:
|
71
|
+
continue
|
72
|
+
dl = d.lower().strip()
|
73
|
+
if any(dl.startswith(h) and len(dl.split()) < 8 for h in heads):
|
74
|
+
continue
|
75
|
+
if len(d) > 10:
|
76
|
+
filtered.append(d)
|
77
|
+
|
78
|
+
if not filtered:
|
79
|
+
filtered = [max(descs, key=len, default="")]
|
80
|
+
|
81
|
+
# Use the longest as the main description
|
82
|
+
main = cls.clean_caption(max(filtered, key=len))
|
83
|
+
parts = [main]
|
84
|
+
seen = set(main.lower().split())
|
85
|
+
|
86
|
+
# Categorize additional descriptions
|
87
|
+
buckets = {
|
88
|
+
"characters": [
|
89
|
+
"character",
|
90
|
+
"person",
|
91
|
+
"animal",
|
92
|
+
"anthro",
|
93
|
+
"wearing",
|
94
|
+
"dressed",
|
95
|
+
],
|
96
|
+
"actions": ["doing", "action", "playing", "running", "sitting", "standing"],
|
97
|
+
"settings": [
|
98
|
+
"room",
|
99
|
+
"outdoor",
|
100
|
+
"indoor",
|
101
|
+
"setting",
|
102
|
+
"background",
|
103
|
+
"environment",
|
104
|
+
],
|
105
|
+
"styles": ["style", "art", "drawn", "sketch", "painted", "digital"],
|
106
|
+
"moods": [
|
107
|
+
"mood",
|
108
|
+
"emotion",
|
109
|
+
"feeling",
|
110
|
+
"atmosphere",
|
111
|
+
"happy",
|
112
|
+
"sad",
|
113
|
+
"angry",
|
114
|
+
],
|
115
|
+
}
|
116
|
+
|
117
|
+
def categorize(text: str) -> str:
|
118
|
+
"""Categorize a description based on keywords."""
|
119
|
+
text_lower = text.lower()
|
120
|
+
for category, keywords in buckets.items():
|
121
|
+
if any(keyword in text_lower for keyword in keywords):
|
122
|
+
return category
|
123
|
+
return "details"
|
124
|
+
|
125
|
+
# Group descriptions by category
|
126
|
+
by_bucket: Dict[str, List[str]] = {}
|
127
|
+
for desc in filtered:
|
128
|
+
category = categorize(desc)
|
129
|
+
by_bucket.setdefault(category, []).append(desc)
|
130
|
+
|
131
|
+
# Add descriptions from each category
|
132
|
+
for category in ["characters", "actions", "settings", "moods", "styles", "details"]:
|
133
|
+
if category in by_bucket and by_bucket[category]:
|
134
|
+
desc = by_bucket[category][0]
|
135
|
+
words = desc.lower().split()
|
136
|
+
|
137
|
+
# Check if this adds enough new information
|
138
|
+
new_words = [w for w in words if w not in seen and len(w) > 3]
|
139
|
+
if len(new_words) > 3:
|
140
|
+
clean = cls.clean_caption(desc)
|
141
|
+
if clean and clean not in parts:
|
142
|
+
parts.append(clean)
|
143
|
+
seen.update(words)
|
144
|
+
|
145
|
+
# Return each part as a separate line for rich captions
|
146
|
+
return "\n".join(parts)
|
147
|
+
|
148
|
+
@staticmethod
|
149
|
+
def validate_caption(caption: str, min_length: int = 20) -> bool:
|
150
|
+
"""Validate if a caption meets quality standards."""
|
151
|
+
if not caption or len(caption) < min_length:
|
152
|
+
return False
|
153
|
+
|
154
|
+
# Check for refusal patterns
|
155
|
+
refusal_patterns = [
|
156
|
+
"i'm sorry",
|
157
|
+
"i cannot",
|
158
|
+
"i apologize",
|
159
|
+
"inappropriate",
|
160
|
+
"unable to",
|
161
|
+
"refuse to",
|
162
|
+
]
|
163
|
+
|
164
|
+
caption_lower = caption.lower()
|
165
|
+
if any(pattern in caption_lower for pattern in refusal_patterns):
|
166
|
+
return False
|
167
|
+
|
168
|
+
# Check for too generic
|
169
|
+
if caption_lower in ["image", "picture", "photo", "illustration"]:
|
170
|
+
return False
|
171
|
+
|
172
|
+
return True
|
@@ -0,0 +1,140 @@
|
|
1
|
+
"""SSL certificate management."""
|
2
|
+
|
3
|
+
import subprocess
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
|
+
from cryptography import x509
|
7
|
+
from cryptography.x509.oid import NameOID
|
8
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
10
|
+
from datetime import datetime, timedelta
|
11
|
+
|
12
|
+
|
13
|
+
class CertificateManager:
|
14
|
+
"""Manages SSL certificate generation."""
|
15
|
+
|
16
|
+
def generate_self_signed(
|
17
|
+
self, output_dir: Path, domain: str = "localhost"
|
18
|
+
) -> tuple[Path, Path]:
|
19
|
+
"""Generate self-signed certificate for development."""
|
20
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
21
|
+
|
22
|
+
# Generate private key
|
23
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
24
|
+
|
25
|
+
# Generate certificate
|
26
|
+
subject = issuer = x509.Name(
|
27
|
+
[
|
28
|
+
x509.NameAttribute(NameOID.COMMON_NAME, domain),
|
29
|
+
]
|
30
|
+
)
|
31
|
+
|
32
|
+
cert = (
|
33
|
+
x509.CertificateBuilder()
|
34
|
+
.subject_name(subject)
|
35
|
+
.issuer_name(issuer)
|
36
|
+
.public_key(key.public_key())
|
37
|
+
.serial_number(x509.random_serial_number())
|
38
|
+
.not_valid_before(datetime.utcnow())
|
39
|
+
.not_valid_after(datetime.utcnow() + timedelta(days=365))
|
40
|
+
.add_extension(
|
41
|
+
x509.SubjectAlternativeName(
|
42
|
+
[
|
43
|
+
x509.DNSName(domain),
|
44
|
+
x509.DNSName("localhost"),
|
45
|
+
x509.DNSName("127.0.0.1"),
|
46
|
+
]
|
47
|
+
),
|
48
|
+
critical=False,
|
49
|
+
)
|
50
|
+
.sign(key, hashes.SHA256())
|
51
|
+
)
|
52
|
+
|
53
|
+
# Write files
|
54
|
+
cert_path = output_dir / "cert.pem"
|
55
|
+
key_path = output_dir / "key.pem"
|
56
|
+
|
57
|
+
with open(cert_path, "wb") as f:
|
58
|
+
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
59
|
+
|
60
|
+
with open(key_path, "wb") as f:
|
61
|
+
f.write(
|
62
|
+
key.private_bytes(
|
63
|
+
encoding=serialization.Encoding.PEM,
|
64
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
65
|
+
encryption_algorithm=serialization.NoEncryption(),
|
66
|
+
)
|
67
|
+
)
|
68
|
+
|
69
|
+
return cert_path, key_path
|
70
|
+
|
71
|
+
def generate_letsencrypt(
|
72
|
+
self, domain: str, email: str, output_dir: Optional[Path] = None, staging: bool = False
|
73
|
+
) -> tuple[Path, Path]:
|
74
|
+
"""
|
75
|
+
Generate Let's Encrypt certificate.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
domain: Domain name for certificate
|
79
|
+
email: Email for Let's Encrypt account
|
80
|
+
output_dir: Custom output directory (uses /etc/letsencrypt by default)
|
81
|
+
staging: Use Let's Encrypt staging server for testing
|
82
|
+
"""
|
83
|
+
cmd = [
|
84
|
+
"certbot",
|
85
|
+
"certonly",
|
86
|
+
"--standalone",
|
87
|
+
"--non-interactive",
|
88
|
+
"--agree-tos",
|
89
|
+
"--email",
|
90
|
+
email,
|
91
|
+
"-d",
|
92
|
+
domain,
|
93
|
+
]
|
94
|
+
|
95
|
+
if staging:
|
96
|
+
cmd.append("--staging")
|
97
|
+
|
98
|
+
if output_dir:
|
99
|
+
# Use custom config and work directories
|
100
|
+
output_dir = Path(output_dir)
|
101
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
102
|
+
cmd.extend(
|
103
|
+
[
|
104
|
+
"--config-dir",
|
105
|
+
str(output_dir),
|
106
|
+
"--work-dir",
|
107
|
+
str(output_dir / "work"),
|
108
|
+
"--logs-dir",
|
109
|
+
str(output_dir / "logs"),
|
110
|
+
]
|
111
|
+
)
|
112
|
+
cert_base = output_dir / "live" / domain
|
113
|
+
else:
|
114
|
+
cert_base = Path(f"/etc/letsencrypt/live/{domain}")
|
115
|
+
|
116
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
117
|
+
if result.returncode != 0:
|
118
|
+
raise RuntimeError(f"Certbot failed: {result.stderr}")
|
119
|
+
|
120
|
+
cert_path = cert_base / "fullchain.pem"
|
121
|
+
key_path = cert_base / "privkey.pem"
|
122
|
+
|
123
|
+
if not cert_path.exists() or not key_path.exists():
|
124
|
+
raise RuntimeError(f"Certificate files not found at {cert_base}")
|
125
|
+
|
126
|
+
return cert_path, key_path
|
127
|
+
|
128
|
+
def get_cert_info(self, cert_path: Path) -> dict:
|
129
|
+
"""Get information about an existing certificate."""
|
130
|
+
with open(cert_path, "rb") as f:
|
131
|
+
cert = x509.load_pem_x509_certificate(f.read())
|
132
|
+
|
133
|
+
return {
|
134
|
+
"subject": cert.subject.rfc4514_string(),
|
135
|
+
"issuer": cert.issuer.rfc4514_string(),
|
136
|
+
"not_before": cert.not_valid_before,
|
137
|
+
"not_after": cert.not_valid_after,
|
138
|
+
"serial_number": cert.serial_number,
|
139
|
+
"is_self_signed": cert.issuer == cert.subject,
|
140
|
+
}
|