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.
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: catime
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: AI-generated hourly cat images - a new cat every hour!
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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">&times;</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: 80vh;
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; }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "catime"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "AI-generated hourly cat images - a new cat every hour!"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- async def generate_cat_image(output_dir: str, timestamp: str) -> dict:
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=f"畫一隻可愛的貓,並在圖片中顯示現在的日期與時間: {timestamp}",
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\n"
151
+ f"**Model:** `{model_used}`\n"
152
+ f"{prompt_line}\n"
121
153
  f"![cat-{number}]({image_url})"
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
- result = asyncio.run(generate_cat_image("/tmp", timestamp))
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