From 39b743d9764cf69b46effe196402e7cd98fd6c92 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Wed, 26 Aug 2026 22:57:52 +0900 Subject: [PATCH] feat(tts): new default tone (1.35/-0.07/-0.30) + collapsible dashboard panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change default voice controls to the user's tuned values: glyph speed 1.35, word_gap -0.07s (-70ms), sentence_gap -0.30s, pitch 0 (env defaults, slider initial values, and the 기본값 reset button all updated). - Make the "봇 목소리(TTS) 조절" dashboard panel collapsible via its header; starts collapsed (▸), expands on click (▾). Co-Authored-By: Claude Opus 4.7 --- wsai/backends/melo.py | 12 +++++----- wsai/dashboard.py | 53 +++++++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/wsai/backends/melo.py b/wsai/backends/melo.py index f402dc3..903873e 100644 --- a/wsai/backends/melo.py +++ b/wsai/backends/melo.py @@ -12,9 +12,9 @@ Env: WSAI_MELO_DEVICE cpu | cuda | auto (default auto: GPU if torch sees one, else CPU; the worker falls back to CPU if CUDA fails) WSAI_TTS_OUT_DIR where wavs are written (default ~/.cache/wsai/tts) - WSAI_TTS_SPEED glyph speed / length_scale multiplier (0.5..2.0, default 1.25) - WSAI_TTS_WORD_GAP intra-sentence pause delta, sec (-0.2..0.5, default 0.25) - WSAI_TTS_SENTENCE_GAP sentence-boundary silence, sec (-0.5..1.5, default 0.75) + WSAI_TTS_SPEED glyph speed / length_scale multiplier (0.5..2.0, default 1.35) + WSAI_TTS_WORD_GAP intra-sentence pause delta, sec (-0.2..0.5, default -0.07) + WSAI_TTS_SENTENCE_GAP sentence-boundary silence, sec (-0.5..1.5, default -0.30) WSAI_TTS_PITCH global semitone offset (-12..12, default 0.0) """ @@ -99,12 +99,12 @@ class MeloTTS: self.out_dir = Path(out_dir or os.environ.get("WSAI_TTS_OUT_DIR") or (Path.home() / ".cache/wsai/tts")) self.speed = float(speed if speed is not None - else os.environ.get("WSAI_TTS_SPEED", "1.25")) + else os.environ.get("WSAI_TTS_SPEED", "1.35")) # Reply-global rhythm/pitch controls (see melo_worker.py / docs manual). self.word_gap = float(word_gap if word_gap is not None - else os.environ.get("WSAI_TTS_WORD_GAP", "0.25")) + else os.environ.get("WSAI_TTS_WORD_GAP", "-0.07")) self.sentence_gap = float(sentence_gap if sentence_gap is not None - else os.environ.get("WSAI_TTS_SENTENCE_GAP", "0.75")) + else os.environ.get("WSAI_TTS_SENTENCE_GAP", "-0.30")) self.pitch = float(pitch if pitch is not None else os.environ.get("WSAI_TTS_PITCH", "0.0")) self.sink = sink or _log_sink diff --git a/wsai/dashboard.py b/wsai/dashboard.py index 3915637..b300f68 100644 --- a/wsai/dashboard.py +++ b/wsai/dashboard.py @@ -463,9 +463,9 @@ class Dashboard: def _tts_values(self) -> dict: t = self.tts return { - "speed": float(getattr(t, "speed", 1.25)), - "word_gap": float(getattr(t, "word_gap", 0.25)), - "sentence_gap": float(getattr(t, "sentence_gap", 0.75)), + "speed": float(getattr(t, "speed", 1.35)), + "word_gap": float(getattr(t, "word_gap", -0.07)), + "sentence_gap": float(getattr(t, "sentence_gap", -0.30)), "pitch": float(getattr(t, "pitch", 0.0)), } @@ -723,6 +723,8 @@ PAGE = r""" .demobar b{color:#ffe08a} .sttbox{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px;margin:0 0 16px} .sttbox h2{font-size:13px;margin:0 0 10px;font-weight:600;color:var(--fg)} + .sttbox h2.collap{cursor:pointer;user-select:none;margin:0} + .sttbox h2.collap #ttsCaret{display:inline-block;width:12px;color:var(--muted)} .sttrow{display:flex;gap:10px;align-items:center;flex-wrap:wrap} .btn{background:#173042;border:1px solid #234a63;color:var(--fg);border-radius:10px;padding:8px 14px;font-size:13px;cursor:pointer} .btn:hover{background:#1d3d54} @@ -865,25 +867,27 @@ PAGE = r"""
@@ -1188,6 +1192,11 @@ function ttsSet(s){ } async function initTts(){ if(ttsInited) return; ttsInited = true; + $('ttsToggle').onclick = ()=>{ + const b=$('ttsBody'), open=b.style.display==='none'; + b.style.display = open ? 'block' : 'none'; + $('ttsCaret').textContent = open ? '▾' : '▸'; + }; ['tSpeed','tWord','tSent','tPitch'].forEach(id => $(id).addEventListener('input', ttsLabels)); $('tPreview').onclick = async ()=>{ const text=($('tText').value||'').trim(); @@ -1211,7 +1220,7 @@ async function initTts(){ else { $('tStat').textContent='적용 실패: '+(j.error||''); } }catch(e){ $('tStat').textContent='적용 오류: '+e; } }; - $('tReset').onclick = ()=>{ ttsSet({speed:1.25,word_gap:0.25,sentence_gap:0.75,pitch:0}); + $('tReset').onclick = ()=>{ ttsSet({speed:1.35,word_gap:-0.07,sentence_gap:-0.3,pitch:0}); $('tStat').textContent='기본값으로 되돌림 (미리듣기/적용을 눌러 반영).'; }; try{ const j=await (await fetch('/api/tts/settings')).json();