catime 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {catime-0.4.0 → catime-0.4.2}/.github/workflows/hourly-cat.yml +2 -2
- {catime-0.4.0 → catime-0.4.2}/PKG-INFO +1 -1
- {catime-0.4.0 → catime-0.4.2}/catlist.json +58 -0
- {catime-0.4.0 → catime-0.4.2}/docs/app.js +16 -0
- {catime-0.4.0 → catime-0.4.2}/docs/index.html +4 -0
- {catime-0.4.0 → catime-0.4.2}/docs/style.css +25 -1
- {catime-0.4.0 → catime-0.4.2}/pyproject.toml +1 -1
- {catime-0.4.0 → catime-0.4.2}/scripts/generate_cat.py +58 -6
- {catime-0.4.0 → catime-0.4.2}/src/catime/cli.py +2 -0
- {catime-0.4.0 → catime-0.4.2}/.github/workflows/publish.yml +0 -0
- {catime-0.4.0 → catime-0.4.2}/.gitignore +0 -0
- {catime-0.4.0 → catime-0.4.2}/LICENSE +0 -0
- {catime-0.4.0 → catime-0.4.2}/README.md +0 -0
- {catime-0.4.0 → catime-0.4.2}/docs/apple-touch-icon.png +0 -0
- {catime-0.4.0 → catime-0.4.2}/docs/favicon-32.png +0 -0
- {catime-0.4.0 → catime-0.4.2}/docs/favicon.ico +0 -0
- {catime-0.4.0 → catime-0.4.2}/docs/icon-192.png +0 -0
- {catime-0.4.0 → catime-0.4.2}/src/catime/__init__.py +0 -0
|
@@ -2,7 +2,7 @@ name: Hourly Cat Generator
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
schedule:
|
|
5
|
-
- cron: '0 * * * *' # Every hour at :00
|
|
5
|
+
- cron: '0,30 * * * *' # Every hour at :00 and :30 (retry)
|
|
6
6
|
workflow_dispatch: # Allow manual trigger
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
python-version: '3.12'
|
|
25
25
|
|
|
26
26
|
- name: Install dependencies
|
|
27
|
-
run: pip install nanobanana-py
|
|
27
|
+
run: pip install nanobanana-py google-genai
|
|
28
28
|
|
|
29
29
|
- name: Generate cat and update repo
|
|
30
30
|
env:
|
|
@@ -438,5 +438,63 @@
|
|
|
438
438
|
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_0226_UTC.png",
|
|
439
439
|
"model": "gemini-3-pro-image-preview",
|
|
440
440
|
"status": "success"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"number": 64,
|
|
444
|
+
"timestamp": "2026-02-02 05:00 UTC",
|
|
445
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_0500_UTC.png",
|
|
446
|
+
"model": "gemini-3-pro-image-preview",
|
|
447
|
+
"status": "success"
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"number": 65,
|
|
451
|
+
"timestamp": "2026-02-02 06:55 UTC",
|
|
452
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_0655_UTC.png",
|
|
453
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
454
|
+
"status": "success"
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
"number": null,
|
|
458
|
+
"timestamp": "2026-02-02 07:45 UTC",
|
|
459
|
+
"url": null,
|
|
460
|
+
"model": "all failed",
|
|
461
|
+
"status": "failed",
|
|
462
|
+
"error": "Failed to generate any images - All models failed. Last error: No image data in response"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"number": 67,
|
|
466
|
+
"timestamp": "2026-02-02 08:35 UTC",
|
|
467
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_0835_UTC.png",
|
|
468
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
469
|
+
"status": "success"
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"number": 68,
|
|
473
|
+
"timestamp": "2026-02-02 09:39 UTC",
|
|
474
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_0939_UTC.png",
|
|
475
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
476
|
+
"status": "success"
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"number": 69,
|
|
480
|
+
"timestamp": "2026-02-02 10:37 UTC",
|
|
481
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_1037_UTC.png",
|
|
482
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
483
|
+
"status": "success"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"number": 70,
|
|
487
|
+
"timestamp": "2026-02-02 11:31 UTC",
|
|
488
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_1131_UTC.png",
|
|
489
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
490
|
+
"status": "success"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"number": 71,
|
|
494
|
+
"timestamp": "2026-02-02 12:21 UTC",
|
|
495
|
+
"prompt": "A cinematic, ultra-high-resolution photograph of a majestic, short-haired black cat with piercing emerald eyes. The cat is poised gracefully on a sleek, minimalist control panel within a futuristic, dimly lit research observatory. Behind the cat, a massive panoramic window reveals a breathtaking view of a swirling nebula in deep space, juxtaposed against the distant, glittering spires of a high-tech metropolis on an exoplanet. The cat's fur is sharply defined, catching the subtle blue and purple glows emanating from the numerous holographic displays and soft LED strips embedded in the console. One prominent, transparent OLED screen, slightly angled towards the viewer, clearly displays the text: '2026-02-02 12:21 UTC' in a crisp, futuristic font, glowing with a soft, warm amber light. The scene is bathed in a dramatic interplay of cool cosmic blues and warm console oranges, creating striking rim lighting on the cat's silhouette. The atmosphere is serene yet profoundly mysterious, suggesting advanced intelligence and cosmic contemplation. Hyperdetailed, volumetric lighting, shallow depth of field, f/1.4, 85mm lens, award-winning photography, professional studio shot, intricate textures, unreal engine 5, octane render, 16k.",
|
|
496
|
+
"url": "https://github.com/yazelin/catime/releases/download/cats/cat_2026-02-02_1221_UTC.png",
|
|
497
|
+
"model": "gemini-2.5-flash-image (fallback from gemini-3-pro-image-preview, reason: API 503: The model is overloaded. Please try again later.)",
|
|
498
|
+
"status": "success"
|
|
441
499
|
}
|
|
442
500
|
]
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
const lbImg = document.getElementById("lb-img");
|
|
19
19
|
const lbInfo = document.getElementById("lb-info");
|
|
20
20
|
const lbClose = document.getElementById("lb-close");
|
|
21
|
+
const lbPrompt = document.getElementById("lb-prompt");
|
|
22
|
+
const lbPromptText = document.getElementById("lb-prompt-text");
|
|
23
|
+
const lbCopyBtn = document.getElementById("lb-copy-btn");
|
|
21
24
|
|
|
22
25
|
// Date picker elements
|
|
23
26
|
const datePickerBtn = document.getElementById("date-picker-btn");
|
|
@@ -214,8 +217,21 @@
|
|
|
214
217
|
function openLightbox(cat) {
|
|
215
218
|
lbImg.src = cat.url;
|
|
216
219
|
lbInfo.textContent = `#${cat.number} \u00b7 ${cat.timestamp} \u00b7 ${cat.model || ""}`;
|
|
220
|
+
if (cat.prompt) {
|
|
221
|
+
lbPromptText.textContent = cat.prompt;
|
|
222
|
+
lbPrompt.classList.remove("hidden");
|
|
223
|
+
lbCopyBtn.textContent = "\u{1f4cb} Copy";
|
|
224
|
+
} else {
|
|
225
|
+
lbPrompt.classList.add("hidden");
|
|
226
|
+
}
|
|
217
227
|
lightbox.classList.remove("hidden");
|
|
218
228
|
}
|
|
229
|
+
lbCopyBtn.addEventListener("click", () => {
|
|
230
|
+
navigator.clipboard.writeText(lbPromptText.textContent).then(() => {
|
|
231
|
+
lbCopyBtn.textContent = "\u2705 Copied!";
|
|
232
|
+
setTimeout(() => { lbCopyBtn.textContent = "\u{1f4cb} Copy"; }, 1500);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
219
235
|
lbClose.addEventListener("click", () => lightbox.classList.add("hidden"));
|
|
220
236
|
lightbox.addEventListener("click", e => { if (e.target === lightbox) lightbox.classList.add("hidden"); });
|
|
221
237
|
document.addEventListener("keydown", e => { if (e.key === "Escape") lightbox.classList.add("hidden"); });
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
<button id="lb-close" aria-label="Close">×</button>
|
|
52
52
|
<img id="lb-img" src="" alt="Cat">
|
|
53
53
|
<div id="lb-info"></div>
|
|
54
|
+
<div id="lb-prompt" class="hidden">
|
|
55
|
+
<p id="lb-prompt-text"></p>
|
|
56
|
+
<button id="lb-copy-btn" title="Copy prompt">📋 Copy</button>
|
|
57
|
+
</div>
|
|
54
58
|
</div>
|
|
55
59
|
|
|
56
60
|
<script src="app.js"></script>
|
|
@@ -255,6 +255,7 @@ body {
|
|
|
255
255
|
backdrop-filter: blur(8px);
|
|
256
256
|
display: flex; flex-direction: column;
|
|
257
257
|
align-items: center; justify-content: center;
|
|
258
|
+
overflow-y: auto; padding: 2rem 0;
|
|
258
259
|
}
|
|
259
260
|
#lb-close {
|
|
260
261
|
position: absolute; top: 1rem; right: 1.2rem;
|
|
@@ -264,7 +265,7 @@ body {
|
|
|
264
265
|
}
|
|
265
266
|
#lb-close:hover { transform: scale(1.2) rotate(90deg); }
|
|
266
267
|
#lb-img {
|
|
267
|
-
max-width: 90vw; max-height:
|
|
268
|
+
max-width: 90vw; max-height: 65vh;
|
|
268
269
|
border-radius: 16px;
|
|
269
270
|
box-shadow: 0 12px 40px rgba(0,0,0,.3);
|
|
270
271
|
}
|
|
@@ -273,6 +274,29 @@ body {
|
|
|
273
274
|
font-weight: 600; text-shadow: 0 1px 4px rgba(0,0,0,.3);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
/* Lightbox prompt */
|
|
278
|
+
#lb-prompt {
|
|
279
|
+
margin-top: .8rem; max-width: 600px; width: 90vw;
|
|
280
|
+
background: rgba(0, 0, 0, .5); border-radius: 12px;
|
|
281
|
+
padding: .7rem 1rem; display: flex; align-items: flex-start; gap: .6rem;
|
|
282
|
+
}
|
|
283
|
+
#lb-prompt-text {
|
|
284
|
+
color: #fff; font-size: .82rem; line-height: 1.5;
|
|
285
|
+
word-break: break-word; flex: 1;
|
|
286
|
+
max-height: 4.5em; overflow-y: auto;
|
|
287
|
+
scrollbar-width: thin; scrollbar-color: var(--pink-light) transparent;
|
|
288
|
+
}
|
|
289
|
+
#lb-prompt-text::-webkit-scrollbar { width: 4px; }
|
|
290
|
+
#lb-prompt-text::-webkit-scrollbar-thumb { background: var(--pink-light); border-radius: 4px; }
|
|
291
|
+
#lb-copy-btn {
|
|
292
|
+
flex-shrink: 0; padding: .3rem .7rem; border: none; border-radius: 20px;
|
|
293
|
+
background: linear-gradient(135deg, var(--pink), var(--purple));
|
|
294
|
+
color: #fff; font-family: inherit; font-size: .78rem; font-weight: 700;
|
|
295
|
+
cursor: pointer; transition: transform .2s, box-shadow .2s; white-space: nowrap;
|
|
296
|
+
box-shadow: 0 2px 8px rgba(255, 107, 157, .3);
|
|
297
|
+
}
|
|
298
|
+
#lb-copy-btn:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(255, 107, 157, .5); }
|
|
299
|
+
|
|
276
300
|
/* Responsive */
|
|
277
301
|
@media (max-width: 1024px) {
|
|
278
302
|
.masonry { column-count: 2; margin-right: 0; }
|
|
@@ -8,11 +8,41 @@ import sys
|
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
PROMPT_META = (
|
|
12
|
+
"You are a professional prompt engineer for AI image generation. "
|
|
13
|
+
"Create a single, detailed English prompt for generating a stunning image. "
|
|
14
|
+
"Requirements: (1) A cat must be the subject or prominently featured "
|
|
15
|
+
"(2) The date and time '{timestamp}' must be visually displayed in the image. "
|
|
16
|
+
"Beyond these two requirements, you have complete creative freedom — surprise me with "
|
|
17
|
+
"varied styles (photography, painting, illustration, etc.), unique scenes, interesting "
|
|
18
|
+
"compositions, lighting, and moods. Do NOT include any resolution keywords "
|
|
19
|
+
"(like 4K, 8K, 16K, etc.) in the prompt. Output ONLY the prompt text, nothing else."
|
|
20
|
+
)
|
|
21
|
+
|
|
11
22
|
REPO = os.environ.get("GITHUB_REPOSITORY", "yazelin/catime")
|
|
12
23
|
RELEASE_TAG = "cats"
|
|
13
24
|
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
def generate_prompt(timestamp: str) -> str:
|
|
27
|
+
"""Use Gemini text model to generate a creative image prompt."""
|
|
28
|
+
try:
|
|
29
|
+
from google import genai
|
|
30
|
+
|
|
31
|
+
client = genai.Client()
|
|
32
|
+
response = client.models.generate_content(
|
|
33
|
+
model="gemini-2.5-flash",
|
|
34
|
+
contents=PROMPT_META.format(timestamp=timestamp),
|
|
35
|
+
)
|
|
36
|
+
prompt = response.text.strip()
|
|
37
|
+
if prompt:
|
|
38
|
+
print(f"AI-generated prompt: {prompt[:120]}...")
|
|
39
|
+
return prompt
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Prompt generation failed ({e}), using fallback.")
|
|
42
|
+
return f"A cute cat with the date and time '{timestamp}' displayed in the image, high quality, detailed"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def generate_cat_image(output_dir: str, timestamp: str, prompt: str) -> dict:
|
|
16
46
|
"""Use nanobanana-py's ImageGenerator to generate a cat image."""
|
|
17
47
|
from nanobanana_py.image_generator import ImageGenerator
|
|
18
48
|
from nanobanana_py.types import ImageGenerationRequest
|
|
@@ -20,7 +50,7 @@ async def generate_cat_image(output_dir: str, timestamp: str) -> dict:
|
|
|
20
50
|
generator = ImageGenerator()
|
|
21
51
|
|
|
22
52
|
request = ImageGenerationRequest(
|
|
23
|
-
prompt=
|
|
53
|
+
prompt=prompt,
|
|
24
54
|
filename=f"cat_{timestamp.replace(' ', '_').replace(':', '')}",
|
|
25
55
|
resolution="1K",
|
|
26
56
|
file_format="png",
|
|
@@ -112,12 +142,14 @@ def get_or_create_monthly_issue(now: datetime) -> str:
|
|
|
112
142
|
return url.split("/")[-1]
|
|
113
143
|
|
|
114
144
|
|
|
115
|
-
def post_issue_comment(issue_number: str, image_url: str, number: int, timestamp: str, model_used: str):
|
|
145
|
+
def post_issue_comment(issue_number: str, image_url: str, number: int, timestamp: str, model_used: str, prompt: str = ""):
|
|
116
146
|
"""Post a comment on the monthly issue with the cat image."""
|
|
147
|
+
prompt_line = f"**Prompt:** {prompt}\n" if prompt else ""
|
|
117
148
|
body = (
|
|
118
149
|
f"## Cat #{number}\n"
|
|
119
150
|
f"**Time:** {timestamp}\n"
|
|
120
|
-
f"**Model:** `{model_used}`\n
|
|
151
|
+
f"**Model:** `{model_used}`\n"
|
|
152
|
+
f"{prompt_line}\n"
|
|
121
153
|
f""
|
|
122
154
|
)
|
|
123
155
|
subprocess.run(
|
|
@@ -158,12 +190,31 @@ def update_catlist_and_push(entry: dict) -> int:
|
|
|
158
190
|
return number or 0
|
|
159
191
|
|
|
160
192
|
|
|
193
|
+
def already_has_cat_this_hour(now: datetime) -> bool:
|
|
194
|
+
"""Check if a successful cat already exists for the current hour."""
|
|
195
|
+
catlist_path = Path("catlist.json")
|
|
196
|
+
if not catlist_path.exists():
|
|
197
|
+
return False
|
|
198
|
+
cats = json.loads(catlist_path.read_text())
|
|
199
|
+
hour_prefix = now.strftime("%Y-%m-%d %H:")
|
|
200
|
+
return any(
|
|
201
|
+
c.get("status", "success") == "success" and c["timestamp"].startswith(hour_prefix)
|
|
202
|
+
for c in cats
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
161
206
|
def main():
|
|
162
207
|
now = datetime.now(timezone.utc)
|
|
163
208
|
timestamp = now.strftime("%Y-%m-%d %H:%M UTC")
|
|
164
209
|
|
|
210
|
+
# Skip if this hour already has a successful cat
|
|
211
|
+
if already_has_cat_this_hour(now):
|
|
212
|
+
print(f"Cat already exists for hour {now.strftime('%Y-%m-%d %H')} UTC, skipping.")
|
|
213
|
+
return
|
|
214
|
+
|
|
165
215
|
print(f"Generating cat for {timestamp}...")
|
|
166
|
-
|
|
216
|
+
prompt = generate_prompt(timestamp)
|
|
217
|
+
result = asyncio.run(generate_cat_image("/tmp", timestamp, prompt))
|
|
167
218
|
|
|
168
219
|
# Read current count for numbering
|
|
169
220
|
catlist_path = Path("catlist.json")
|
|
@@ -197,6 +248,7 @@ def main():
|
|
|
197
248
|
entry = {
|
|
198
249
|
"number": next_number,
|
|
199
250
|
"timestamp": timestamp,
|
|
251
|
+
"prompt": prompt,
|
|
200
252
|
"url": image_url,
|
|
201
253
|
"model": model_used,
|
|
202
254
|
"status": "success",
|
|
@@ -207,7 +259,7 @@ def main():
|
|
|
207
259
|
print("Posting issue comment...")
|
|
208
260
|
issue_number = get_or_create_monthly_issue(now)
|
|
209
261
|
print(f"Using monthly issue #{issue_number}")
|
|
210
|
-
post_issue_comment(issue_number, image_url, next_number, timestamp, model_used)
|
|
262
|
+
post_issue_comment(issue_number, image_url, next_number, timestamp, model_used, prompt)
|
|
211
263
|
|
|
212
264
|
print(f"Done! Cat #{next_number}")
|
|
213
265
|
|
|
@@ -36,6 +36,8 @@ def print_cat(cat: dict, index: int | None = None):
|
|
|
36
36
|
else:
|
|
37
37
|
print(f"Cat #{num:>4} {cat['timestamp']} model: {cat.get('model', '?')}")
|
|
38
38
|
print(f" URL: {cat['url']}")
|
|
39
|
+
if cat.get("prompt"):
|
|
40
|
+
print(f" Prompt: {cat['prompt']}")
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
def filter_by_query(cats: list[dict], query: str) -> list[dict]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|