yycc commited on
Commit
779d05e
·
verified ·
1 Parent(s): 01a6bcd

Usage counter: append one metadata-only JSON line per generation to a private dataset (all replicas)

Browse files
Files changed (1) hide show
  1. app.py +37 -2
app.py CHANGED
@@ -25,6 +25,7 @@ import io
25
  import json
26
  import random
27
  import re
 
28
  import time
29
  from pathlib import Path
30
 
@@ -35,6 +36,7 @@ import compare
35
  from PIL import Image, ImageOps
36
  from diffusers import FlowMatchEulerDiscreteScheduler, QwenImage21Pipeline, QwenImage21Transformer2DModel
37
  from diffusers.pipelines.qwenimage21.pipeline_qwenimage21 import calculate_dimensions
 
38
 
39
  # Students in the model repo. STUDENT="lora" (default): the v0.2.1 LoRA (r256) at the repo root, applied at runtime on
40
  # top of the base transformer and never merged (merging into bf16 keeps only ~47% of the adapter delta, the
@@ -47,6 +49,30 @@ STUDENT = os.environ.get("STUDENT", "lora")
47
  LORA_FILE = os.environ.get("LORA_FILE", "Qwen-Image-2.1-viggle-turbo-v0.2.1-6step-lora-r256.safetensors")
48
  STUDENT_TAG = {"full": "v0.1 full fine-tune (transformer/)", "lora": "v0.2.1 LoRA r256" if "v0.2.1" in LORA_FILE else f"LoRA {LORA_FILE}"}[STUDENT]
49
  HF_TOKEN = os.environ.get("HF_TOKEN") # only needed while the model repo is private
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # The LoRA is sampled on 6 Euler steps whose raw (pre-shift) sigma nodes are RAW_NODES: the 4-step training schedule
51
  # linspace(1, 1/4, 4) with its highest-noise segment [1, 0.75] cut into three (1, 0.9375, 0.875, 0.75). The composition
52
  # is decided in that segment, and one big Euler step there ghosts and drifts the layout; the low-noise nodes 0.75, 0.5,
@@ -209,6 +235,7 @@ def denoise(prompt, images, width, height, steps, seed):
209
 
210
 
211
  def generate(prompt, refs, size_label, seed, randomize_seed, enhance="Auto", steps=STEPS, custom_width=1024, custom_height=1024):
 
212
  steps = min(max(int(steps), 3), 8) # 6 (RAW_NODES) is the validated schedule; 3-8 are accepted, fewer or more degrade quickly
213
  # refs: the gallery's (filepath, caption) pairs in upload order, which is the order the prompt's "image 1, 2, ..." refer
214
  # to. EXIF rotation + RGB is what the gr.Image slots used to do.
@@ -243,11 +270,19 @@ def generate(prompt, refs, size_label, seed, randomize_seed, enhance="Auto", ste
243
  used_prompt = enhance_prompt(prompt, images, ratio_name) or prompt
244
  pe_time = time.perf_counter() - started
245
  enhance_note = f" · enhanced {pe_time:.1f}s" if used_prompt != prompt else " · enhancement failed, prompt sent as written"
246
- result, elapsed = denoise(used_prompt, images, width, height, steps, seed)
 
 
 
 
 
 
247
  # one line per generation, for counting usage from the Space logs (no prompt text)
248
- print(f"[gen] {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} mode={'edit' if images else 't2i'} refs={len(images)} "
249
  f"steps={steps} size={result.width}x{result.height} enhance={int(used_prompt != prompt)} pe={pe_time:.1f}s gen={elapsed:.2f}s",
250
  flush=True)
 
 
251
  return result, f"seed `{seed}` · {result.width}×{result.height} · {steps} steps · {elapsed:.2f}s · {STUDENT_TAG}{enhance_note}{clamped}", used_prompt
252
 
253
 
 
25
  import json
26
  import random
27
  import re
28
+ import socket
29
  import time
30
  from pathlib import Path
31
 
 
36
  from PIL import Image, ImageOps
37
  from diffusers import FlowMatchEulerDiscreteScheduler, QwenImage21Pipeline, QwenImage21Transformer2DModel
38
  from diffusers.pipelines.qwenimage21.pipeline_qwenimage21 import calculate_dimensions
39
+ from huggingface_hub import CommitScheduler
40
 
41
  # Students in the model repo. STUDENT="lora" (default): the v0.2.1 LoRA (r256) at the repo root, applied at runtime on
42
  # top of the base transformer and never merged (merging into bf16 keeps only ~47% of the adapter delta, the
 
49
  LORA_FILE = os.environ.get("LORA_FILE", "Qwen-Image-2.1-viggle-turbo-v0.2.1-6step-lora-r256.safetensors")
50
  STUDENT_TAG = {"full": "v0.1 full fine-tune (transformer/)", "lora": "v0.2.1 LoRA r256" if "v0.2.1" in LORA_FILE else f"LoRA {LORA_FILE}"}[STUDENT]
51
  HF_TOKEN = os.environ.get("HF_TOKEN") # only needed while the model repo is private
52
+ # Usage counter. The Space autoscales to several replicas and its log view shows only one of them, so the [gen] log lines
53
+ # undercount. Every generate() call that reaches the GPU also appends one JSON line (metadata only: no prompt, no images)
54
+ # to this container's own file, which CommitScheduler pushes to a private dataset every 10 minutes and at exit; one file
55
+ # per container, so replicas never write the same path. The token is STATS_TOKEN, else HF_TOKEN; it needs write access to
56
+ # that dataset. Without a token, or if the scheduler cannot start with it, the counter is off and the app runs as before.
57
+ STATS_DIR = Path("/tmp/gen_stats")
58
+ STATS_DIR.mkdir(exist_ok=True)
59
+ STATS_FILE = STATS_DIR / f"{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{socket.gethostname()}.jsonl"
60
+ STATS_TOKEN = os.environ.get("STATS_TOKEN") or HF_TOKEN
61
+ stats = None
62
+ if STATS_TOKEN:
63
+ try:
64
+ stats = CommitScheduler(repo_id="Viggle/qwen-image-21-turbo-space-stats", repo_type="dataset", folder_path=STATS_DIR,
65
+ path_in_repo="gens", every=10, private=True, token=STATS_TOKEN)
66
+ except Exception as e: # e.g. a token that cannot see the dataset: count nothing rather than fail to start
67
+ print(f"[stats] off: {type(e).__name__}: {e}", flush=True)
68
+
69
+
70
+ def record(**row):
71
+ if stats:
72
+ with stats.lock, STATS_FILE.open("a") as f:
73
+ f.write(json.dumps({"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), **row}) + "\n")
74
+
75
+
76
  # The LoRA is sampled on 6 Euler steps whose raw (pre-shift) sigma nodes are RAW_NODES: the 4-step training schedule
77
  # linspace(1, 1/4, 4) with its highest-noise segment [1, 0.75] cut into three (1, 0.9375, 0.875, 0.75). The composition
78
  # is decided in that segment, and one big Euler step there ghosts and drifts the layout; the low-noise nodes 0.75, 0.5,
 
235
 
236
 
237
  def generate(prompt, refs, size_label, seed, randomize_seed, enhance="Auto", steps=STEPS, custom_width=1024, custom_height=1024):
238
+ t0 = time.perf_counter()
239
  steps = min(max(int(steps), 3), 8) # 6 (RAW_NODES) is the validated schedule; 3-8 are accepted, fewer or more degrade quickly
240
  # refs: the gallery's (filepath, caption) pairs in upload order, which is the order the prompt's "image 1, 2, ..." refer
241
  # to. EXIF rotation + RGB is what the gr.Image slots used to do.
 
270
  used_prompt = enhance_prompt(prompt, images, ratio_name) or prompt
271
  pe_time = time.perf_counter() - started
272
  enhance_note = f" · enhanced {pe_time:.1f}s" if used_prompt != prompt else " · enhancement failed, prompt sent as written"
273
+ mode = "edit" if images else "t2i"
274
+ try:
275
+ result, elapsed = denoise(used_prompt, images, width, height, steps, seed)
276
+ except Exception as e: # ZeroGPU quota / GPU timeout errors land here: count them, then show them as before
277
+ record(ok=False, mode=mode, refs=len(images), steps=steps, error=f"{type(e).__name__}: {e}"[:200],
278
+ total=round(time.perf_counter() - t0, 2))
279
+ raise
280
  # one line per generation, for counting usage from the Space logs (no prompt text)
281
+ print(f"[gen] {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} mode={mode} refs={len(images)} "
282
  f"steps={steps} size={result.width}x{result.height} enhance={int(used_prompt != prompt)} pe={pe_time:.1f}s gen={elapsed:.2f}s",
283
  flush=True)
284
+ record(ok=True, mode=mode, refs=len(images), steps=steps, size=f"{result.width}x{result.height}", enhance=enhance,
285
+ enhanced=used_prompt != prompt, pe=round(pe_time, 2), gen=round(elapsed, 2), total=round(time.perf_counter() - t0, 2))
286
  return result, f"seed `{seed}` · {result.width}×{result.height} · {steps} steps · {elapsed:.2f}s · {STUDENT_TAG}{enhance_note}{clamped}", used_prompt
287
 
288