File size: 2,668 Bytes
261e6ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
"""voice_daemon.py - resident Cantonese TTS server for LYRA.
Runs in the isolated `envs/voice` environment (qwen-tts, transformers 4.57.3)."""
import json, os, sys, time, traceback
from pathlib import Path
import numpy as np, soundfile as sf, torch
sys.path.insert(0, "/marimo/ai_studio")
from studio_core import BASE, JOBS, JOBS_DONE, RESULTS, LOGS, VOICE_MODEL

MODEL = None
def log(m):
    line = f"[{time.strftime('%H:%M:%S')}] {m}"
    print(line, flush=True)
    with open(LOGS/"voice_daemon.log", "a") as f: f.write(line + "\n")

def get_model():
    global MODEL
    if MODEL is None:
        from qwen_tts import Qwen3TTSModel
        log("load Qwen3-TTS + Cantonese LoRA ...")
        MODEL = Qwen3TTSModel.from_pretrained(str(VOICE_MODEL), device_map="cuda", dtype=torch.bfloat16)
        log(f"voice ready | speakers={MODEL.get_supported_speakers()} | langs={MODEL.get_supported_languages()}")
    return MODEL

def job_tts(j):
    m = get_model()
    spk = j.get("speaker") or (m.get_supported_speakers() or [None])[0]
    kw = dict(text=j["text"], speaker=spk, language=j.get("language") or "chinese")
    if j.get("instruct"): kw["instruct"] = j["instruct"]
    log(f"tts '{j['name']}' spk={spk} :: {j['text'][:30]}")
    t0 = time.time()
    wavs, sr = m.generate_custom_voice(**kw)
    out = BASE/"assets"/"audio"/j["name"]
    sf.write(str(out), np.asarray(wavs[0]), sr)
    dur = len(np.asarray(wavs[0]))/sr
    log(f"wrote {out.name} {dur:.2f}s in {time.time()-t0:.1f}s")
    return {"files": [str(out)], "seconds": round(dur,2), "speaker": spk}

HANDLERS = {"tts": job_tts, "ping": lambda j: {"pong": True}}

def main():
    log(f"voice daemon up pid={os.getpid()}")
    while True:
        found = False
        for f in sorted(JOBS.glob("*.json")):
            try: j = json.loads(f.read_text())
            except Exception: continue
            if j.get("type") not in HANDLERS:     # not ours -> leave it
                continue
            found = True; jid = j.get("id", f.stem)
            try:
                res = {"id": jid, "status": "ok", **(HANDLERS[j["type"]](j) or {})}
            except Exception as e:
                res = {"id": jid, "status": "error", "error": f"{type(e).__name__}: {e}",
                       "trace": traceback.format_exc()[-1200:]}
                log("FAILED " + res["error"])
            tmp = RESULTS/f".{res['id']}.tmp"; tmp.write_text(json.dumps(res, ensure_ascii=False, indent=2))
            tmp.rename(RESULTS/f"{res['id']}.json"); f.rename(JOBS_DONE/f.name)
            log(f"done {res['id']} status={res['status']}")
        if not found: time.sleep(1)

if __name__ == "__main__":
    main()