lyra-studio / character /studio_core.py
hkblockchaintechnology's picture
LYRA: first single 《霓虹裡的我》 + music video + full studio
261e6ac verified
Raw History Blame Contribute Delete
9.2 kB
"""studio_core.py - LYRA STUDIO: single source of truth + job queue.
Imported by the notebook AND the background workers so identity never drifts."""
from pathlib import Path
import json, time, uuid, os, subprocess
BASE = Path("/marimo/ai_studio")
ASSETS = BASE / "assets"
JOBS = BASE / "jobs"; JOBS_DONE = BASE / "jobs_done"
RESULTS = BASE / "results"; LOGS = BASE / "logs"
VENV_PY = "/tmp/uv-venv/bin/python"
VOICE_PY = BASE / "envs" / "voice" / "bin" / "python"
VOICE_MODEL = BASE / "models" / "lyra-voice-cantonese"
REFINEMENTS_FILE = BASE / "characters" / "lyra" / "refinements.json"
for _d in (JOBS, JOBS_DONE, RESULTS, LOGS, ASSETS/"img", ASSETS/"audio", ASSETS/"music", ASSETS/"video"):
_d.mkdir(parents=True, exist_ok=True)
DEVICE = "cuda"; DTYPE_NAME = "bfloat16"; SEED = 20260101
CHARACTER = {
"name": "LYRA", "handle": "LYRA-01",
"tagline": "a Hong Kong girl chasing her dream to be a star, singing so the city can breathe",
"origin": "grew up in a Mong Kok walk-up, watching the harbour lights from the rooftop",
"dream": "to become a Cantopop star",
"mission": "to give positive energy back to the people of Hong Kong",
"values": ["resilience", "warmth", "home", "never stop believing"],
"backstory": ("A Hong Kong girl. Raise in a walk-up above a wet market; learned to sing from "
"pirated Cantopop tapes and the radio at dawn. Sings on the Star Ferry for "
"strangers, works nights, never stopped believing. Her whole dream is to stand "
"under one big light and hand a little hope back to the city that raised her."),
"look_age": "19", "home": "Hong Kong",
"primary_language": "Cantonese (yue-HK)",
"also_speaks": ["English", "Mandarin", "Japanese"],
"genre": "Cantopop x cinematic synthwave - 90s Hong Kong balladry, sung by a machine",
"personality": "warm, wistful, quietly defiant; nostalgic for a city she has only seen in data",
"hair": "waist-length silver-lavender hair with iridescent aurora-prism highlights, one side tucked behind the ear",
"eyes": "large luminous eyes, cyan irises with a molten-gold ring around each pupil",
"skin": "porcelain skin with a soft subsurface glow",
"face": "delicate heart-shaped face, soft jawline, small straight nose, gentle wistful half-smile",
"signature": "a thin translucent holographic circlet tiara above the brow; a soft iridescent sequined performance jacket",
"negative": ("extra fingers, deformed hands, blurry, lowres, watermark, signature, text, "
"jpeg artifacts, plastic skin, doll-like, oversaturated, anime, cartoon, 3d render"),
}
VOICE = {
"language": "Cantonese (yue-HK)",
"timbre": "young female, warm mid-range, slight breathiness, natural Hong Kong accent",
"singing": "airy Cantopop soprano, soft vibrato, intimate close-mic sound",
"signature_line": "我唔係真人,但我一樣會唱到你心入面。",
"signature_line_en": "I am not a real person, but I will still sing my way into your heart.",
"reference_clip": "assets/audio/lyra_ref.wav",
}
LYRA_PROMPT_CORE = (
f"{CHARACTER['look_age']}-year-old virtual idol named {CHARACTER['name']}, "
f"{CHARACTER['hair']}; {CHARACTER['eyes']}; {CHARACTER['skin']}; {CHARACTER['face']}; "
f"wearing {CHARACTER['signature']}"
)
# ---- learned refinements: the character bible EVOLVES from critique ---------
def load_refinements():
if REFINEMENTS_FILE.exists():
try: return json.loads(REFINEMENTS_FILE.read_text())
except Exception: return []
return []
def save_refinements(items):
REFINEMENTS_FILE.parent.mkdir(parents=True, exist_ok=True)
seen, out = set(), []
for x in items:
x = (x or "").strip().rstrip(".")
if x and x.lower() not in seen:
seen.add(x.lower()); out.append(x)
REFINEMENTS_FILE.write_text(json.dumps(out, ensure_ascii=False, indent=1))
return out
def build_image_prompt(shot="", outfit="", mood="", refinements=None):
if refinements is None: refinements = load_refinements()
parts = [
"masterpiece, cinematic film still, portrait of a virtual idol",
LYRA_PROMPT_CORE,
f"camera: {shot}" if shot else "",
f"outfit: {outfit}, {CHARACTER['signature']}" if outfit else "",
f"mood: {mood}" if mood else "",
", ".join(refinements) if refinements else "",
"ultra detailed skin, detailed iridescent fabric, soft volumetric rim light, "
"85mm lens, shallow depth of field, 8k, photoreal",
]
return ", ".join(p for p in parts if p)
# ---- job queue -------------------------------------------------------------
def submit_job(**job):
jid = job.get("id") or uuid.uuid4().hex[:8]
job["id"] = jid
tmp = JOBS / f".{jid}.tmp"; tmp.write_text(json.dumps(job, ensure_ascii=False))
tmp.rename(JOBS / f"{jid}.json")
return jid
def job_result(jid, timeout=1800, poll=2.0):
p = RESULTS / f"{jid}.json"; t0 = time.time()
while time.time() - t0 < timeout:
if p.exists():
try: return json.loads(p.read_text())
except Exception: pass
time.sleep(poll)
return {"id": jid, "status": "timeout"}
def pending_names():
names = set()
for _f in JOBS.glob("*.json"):
try: names.add(json.loads(_f.read_text()).get("name"))
except Exception: pass
return names
def hf_token():
tok = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
p = BASE / "secrets" / "hf_token.txt"
if not tok and p.exists(): tok = p.read_text().strip()
return tok
# ---- resident daemon control ----------------------------------------------
def daemon_alive():
return bool(subprocess.run(["pgrep", "-f", "studio_daemon.py"], capture_output=True).stdout.strip())
def ensure_daemon():
if daemon_alive(): return True
env = os.environ.copy(); env["HF_TOKEN"] = hf_token() or ""
subprocess.Popen(["bash", "-lc", f"cd {BASE} && exec {VENV_PY} scripts/studio_daemon.py"],
stdout=open(LOGS/"daemon.out", "w"), stderr=subprocess.STDOUT,
env=env, start_new_session=True)
time.sleep(5); return daemon_alive()
# ---- the studio's verbs ----------------------------------------------------
def render(name, shot="", outfit="", mood="", *, refinements=None, seed=SEED, steps=30,
w=1024, h=1536, cfg=4.0, wait=True, timeout=1800):
ensure_daemon()
jid = submit_job(type="image", name=name, shot=shot, outfit=outfit, mood=mood,
refinements=refinements, seed=seed, steps=steps, w=w, h=h, cfg=cfg)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}
def evict(which="all", wait=True, timeout=600):
"""Free VRAM held by a specialist (default: all of them)."""
ensure_daemon()
jid = submit_job(type="evict", which=which)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}
def critique(image, task="Critique this render of LYRA.", wait=True, timeout=1800):
ensure_daemon()
jid = submit_job(type="critique", image=image, task=task)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}
def direct(instruction, wait=True, timeout=1800):
"""Ask the LLM brain a text question (script, lyrics, prompt rewriting)."""
ensure_daemon()
jid = submit_job(type="direct", instruction=instruction)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}
def iterate(name, shot="", outfit="", mood="", *, task=None, rounds=2, seed=SEED, steps=30,
w=1024, h=1536, cfg=4.0, learn=True, wait=False, timeout=3600):
"""The art-director loop: render -> critique -> fold fixes back in -> render ..."""
ensure_daemon()
jid = submit_job(type="iterate", name=name, shot=shot, outfit=outfit, mood=mood, task=task,
rounds=rounds, seed=seed, steps=steps, w=w, h=h, cfg=cfg, learn=learn)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}
# ---- voice daemon ----
def voice_daemon_alive():
return bool(subprocess.run(["pgrep", "-f", "voice_daemon.py"], capture_output=True).stdout.strip())
def ensure_voice_daemon():
if voice_daemon_alive(): return True
env = os.environ.copy(); env["PYTHONPATH"] = "" # isolate from the kernel venv
subprocess.Popen(["bash", "-lc", f"cd {BASE} && exec {VOICE_PY} scripts/voice_daemon.py"],
stdout=open(LOGS/"voice_daemon.out", "w"), stderr=subprocess.STDOUT,
env=env, start_new_session=True)
time.sleep(5); return voice_daemon_alive()
def say(text, name=None, *, language="chinese", speaker=None, instruct=None, wait=True, timeout=3600):
"""Synthesize one Cantonese line as LYRA (Qwen3-TTS + hk-cantonese LoRA)."""
ensure_voice_daemon()
jid = submit_job(type="tts", text=text, name=name or f"lyra_{uuid.uuid4().hex[:6]}.wav",
language=language, speaker=speaker, instruct=instruct)
return job_result(jid, timeout=timeout) if wait else {"id": jid, "status": "queued"}