feat: add Seed Finder panel to voice inspector
Adds a collapsible "🎲 Seed Finder" panel to the voice inspector pane.
Select a voice, expand the panel, set a seed range (default 1–15), and
click Run to generate audio samples with different seeds.
Each result row has Play and "Use this seed" buttons. "Use this seed"
saves the chosen seed to the faster-qwen3-tts voices.json via the new
/api/tts-voice-seed proxy endpoint (which calls /voice-seed on the TTS
server). The saved row gets a ✓ marker.
Test sentence auto-selects DE/EN mixed or EN-only based on voice name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d4b176e4d1
commit
fff6e7e096
@ -458,6 +458,27 @@ async def tts_preview(request: Request):
|
|||||||
backend = _clean_preview_backend(data.get("backend", "voice_clone"))
|
backend = _clean_preview_backend(data.get("backend", "voice_clone"))
|
||||||
settings = _load_settings()
|
settings = _load_settings()
|
||||||
|
|
||||||
|
# Per-request generation overrides (seed / temperature / top_p). When provided,
|
||||||
|
# force them onto the stability params for this backend so _apply_tts_extra_params
|
||||||
|
# forwards them to the engine (backends that reject them fall back gracefully).
|
||||||
|
_overrides = {}
|
||||||
|
for _k in ("seed", "temperature", "top_p"):
|
||||||
|
_v = data.get(_k)
|
||||||
|
if _v is not None and _v != "":
|
||||||
|
try:
|
||||||
|
_overrides[_k] = int(_v) if _k == "seed" else float(_v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if _overrides:
|
||||||
|
from core.config import _tts_extra_params
|
||||||
|
settings = dict(settings)
|
||||||
|
eff = dict(_tts_extra_params(settings, backend) or {})
|
||||||
|
eff.update(_overrides)
|
||||||
|
settings["tts_stability_enabled"] = True
|
||||||
|
by_backend = dict(settings.get("tts_extra_params_by_backend") or {})
|
||||||
|
by_backend[backend] = eff
|
||||||
|
settings["tts_extra_params_by_backend"] = by_backend
|
||||||
|
|
||||||
if data.get("apply_persona"):
|
if data.get("apply_persona"):
|
||||||
scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT))
|
scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT))
|
||||||
wav = _find_voice_audio(voice, scan_dir)
|
wav = _find_voice_audio(voice, scan_dir)
|
||||||
@ -854,3 +875,32 @@ async def openai_speech_proxy(request: Request):
|
|||||||
@router.post("/v1")
|
@router.post("/v1")
|
||||||
async def openai_speech_proxy_v1_shortcut(request: Request):
|
async def openai_speech_proxy_v1_shortcut(request: Request):
|
||||||
return await openai_speech_proxy(request)
|
return await openai_speech_proxy(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/tts-voice-seed")
|
||||||
|
async def tts_voice_seed(request: Request):
|
||||||
|
"""Proxy to the faster-qwen3-tts /voice-seed endpoint.
|
||||||
|
|
||||||
|
Writes or clears the seed for a voice in the TTS server's voices.json.
|
||||||
|
Body: {"voice": "EN_F_NatashaNeural", "seed": 7} (seed: null to remove)
|
||||||
|
"""
|
||||||
|
data = await request.json()
|
||||||
|
settings = _load_settings()
|
||||||
|
tts_base = _preview_backend_base_url(settings, "voice_clone").rstrip("/")
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{tts_base}/voice-seed",
|
||||||
|
json=data,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
raise HTTPException(502, "Could not reach TTS server")
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
detail = ""
|
||||||
|
try:
|
||||||
|
detail = e.response.json().get("detail", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise HTTPException(e.response.status_code, detail or str(e))
|
||||||
|
|||||||
213
static/js/seed-finder.js
Normal file
213
static/js/seed-finder.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
// ── Seed Finder — find the best RNG seed for a voice ─────────────────────────
|
||||||
|
//
|
||||||
|
// Appended to the voice inspector body by voice-inspector.js.
|
||||||
|
// Generates audio samples for a voice across a range of seeds so the user
|
||||||
|
// can listen and pick the most natural-sounding one, then save it to the
|
||||||
|
// TTS server's voices.json with one click.
|
||||||
|
|
||||||
|
const SEED_FINDER_TEXT_DE = (
|
||||||
|
'Die 3.500 neuen High-End Geräte für das Server-Update benötigen eine ' +
|
||||||
|
'außergewöhnlich starke Kühlung und regelmäßige Maßnahmen, um die Performance ' +
|
||||||
|
'bei großer Last zu gewährleisten.'
|
||||||
|
);
|
||||||
|
const SEED_FINDER_TEXT_EN = (
|
||||||
|
'The system administrator successfully configured the customized Docker stacks ' +
|
||||||
|
'and benchmarked the inference engines at exactly 8:45 AM.'
|
||||||
|
);
|
||||||
|
const SEED_FINDER_TEXT_MIXED = SEED_FINDER_TEXT_DE + ' - ' + SEED_FINDER_TEXT_EN;
|
||||||
|
|
||||||
|
function _seedFinderDefaultText(voiceId) {
|
||||||
|
const lc = (voiceId || '').toLowerCase();
|
||||||
|
if (lc.startsWith('de_')) return SEED_FINDER_TEXT_MIXED;
|
||||||
|
if (lc.startsWith('en_') || lc.startsWith('gb_')) return SEED_FINDER_TEXT_EN;
|
||||||
|
return SEED_FINDER_TEXT_MIXED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from voice-inspector.js after the persona panel is appended.
|
||||||
|
function attachSeedFinder(voiceId, body) {
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'opt-group seed-finder-group';
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="opt-group-title">
|
||||||
|
<span class="opt-chevron"></span>
|
||||||
|
<span class="opt-title-text">🎲 Seed Finder</span>
|
||||||
|
<span class="opt-group-meta">Find the most natural-sounding voice</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt-group-subtitle">
|
||||||
|
Generates audio for each seed so you can listen and pick the best one.
|
||||||
|
Click "Use this seed" to save it to the TTS server.
|
||||||
|
</div>
|
||||||
|
<div class="opt-group-body seed-finder-body">
|
||||||
|
<div class="seed-finder-config">
|
||||||
|
<div class="seed-finder-row">
|
||||||
|
<label class="seed-finder-label">Test sentence</label>
|
||||||
|
<textarea class="seed-finder-text" rows="3" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="seed-finder-row seed-finder-params">
|
||||||
|
<div class="seed-finder-field">
|
||||||
|
<label>Seeds: from</label>
|
||||||
|
<input class="seed-finder-from" type="number" min="0" max="9999" value="1" step="1">
|
||||||
|
<label>to</label>
|
||||||
|
<input class="seed-finder-to" type="number" min="1" max="9999" value="15" step="1">
|
||||||
|
</div>
|
||||||
|
<div class="seed-finder-field">
|
||||||
|
<label>Backend</label>
|
||||||
|
<select class="seed-finder-backend">
|
||||||
|
<option value="voice_clone" selected>Voice Clone</option>
|
||||||
|
<option value="streaming">Streaming</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="seed-finder-actions">
|
||||||
|
<button class="btn-primary seed-finder-run-btn">▶ Run Seed Finder</button>
|
||||||
|
<button class="btn-secondary seed-finder-cancel-btn" style="display:none">✕ Cancel</button>
|
||||||
|
<span class="seed-finder-status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="seed-finder-progress" style="display:none">
|
||||||
|
<div class="seed-finder-progress-head">
|
||||||
|
<span class="seed-finder-progress-label">Generating…</span>
|
||||||
|
<span class="seed-finder-progress-count">0 / 0</span>
|
||||||
|
</div>
|
||||||
|
<div class="benchmark-progress-track">
|
||||||
|
<div class="seed-finder-bar" style="width:0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="seed-finder-results"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
body.appendChild(panel);
|
||||||
|
|
||||||
|
// Toggle open/closed
|
||||||
|
panel.querySelector('.opt-group-title').addEventListener('click', () => {
|
||||||
|
panel.classList.toggle('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
const textEl = panel.querySelector('.seed-finder-text');
|
||||||
|
const fromEl = panel.querySelector('.seed-finder-from');
|
||||||
|
const toEl = panel.querySelector('.seed-finder-to');
|
||||||
|
const backendEl = panel.querySelector('.seed-finder-backend');
|
||||||
|
const runBtn = panel.querySelector('.seed-finder-run-btn');
|
||||||
|
const cancelBtn = panel.querySelector('.seed-finder-cancel-btn');
|
||||||
|
const statusEl = panel.querySelector('.seed-finder-status');
|
||||||
|
const progressEl = panel.querySelector('.seed-finder-progress');
|
||||||
|
const progLabel = panel.querySelector('.seed-finder-progress-label');
|
||||||
|
const progCount = panel.querySelector('.seed-finder-progress-count');
|
||||||
|
const progBar = panel.querySelector('.seed-finder-bar');
|
||||||
|
const resultsEl = panel.querySelector('.seed-finder-results');
|
||||||
|
|
||||||
|
textEl.value = _seedFinderDefaultText(voiceId);
|
||||||
|
|
||||||
|
let _cancelled = false;
|
||||||
|
let _currentAudio = null;
|
||||||
|
|
||||||
|
function _playBlob(blob) {
|
||||||
|
if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; }
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const audio = new Audio(url);
|
||||||
|
_currentAudio = audio;
|
||||||
|
audio.play().catch(() => {});
|
||||||
|
audio.addEventListener('ended', () => { URL.revokeObjectURL(url); _currentAudio = null; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _addResultRow(seed, blob, durationSec, err, activeSeedSet) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'seed-finder-result-row' + (err ? ' seed-finder-result-error' : '');
|
||||||
|
row.dataset.seed = seed;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
row.innerHTML = `
|
||||||
|
<span class="sfr-seed">Seed ${seed}</span>
|
||||||
|
<span class="sfr-error">Failed: ${escHtml(String(err))}</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const durStr = durationSec ? `${durationSec.toFixed(1)}s` : '';
|
||||||
|
row.innerHTML = `
|
||||||
|
<span class="sfr-seed">Seed ${seed}</span>
|
||||||
|
<span class="sfr-dur">${durStr}</span>
|
||||||
|
<button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
|
||||||
|
<button class="btn-primary sfr-use-btn">★ Use seed ${seed}</button>
|
||||||
|
<span class="sfr-saved"></span>
|
||||||
|
`;
|
||||||
|
row.querySelector('.sfr-play-btn').addEventListener('click', () => _playBlob(blob));
|
||||||
|
row.querySelector('.sfr-use-btn').addEventListener('click', async () => {
|
||||||
|
// Stop any playback
|
||||||
|
if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; }
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/tts-voice-seed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ voice: voiceId, seed }),
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const e = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(e.detail || resp.statusText);
|
||||||
|
}
|
||||||
|
// Clear all other "saved" markers
|
||||||
|
resultsEl.querySelectorAll('.sfr-saved').forEach(el => el.textContent = '');
|
||||||
|
resultsEl.querySelectorAll('.seed-finder-result-row').forEach(r => r.classList.remove('sfr-active'));
|
||||||
|
row.classList.add('sfr-active');
|
||||||
|
row.querySelector('.sfr-saved').textContent = '✓ Saved';
|
||||||
|
if (typeof toast === 'function') toast(`Seed ${seed} saved for ${voiceId}`, 'success');
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof toast === 'function') toast('Save failed: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsEl.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _runFinder() {
|
||||||
|
const text = textEl.value.trim();
|
||||||
|
if (!text) { statusEl.textContent = 'Enter a test sentence.'; return; }
|
||||||
|
|
||||||
|
const seedFrom = Math.max(0, parseInt(fromEl.value, 10) || 1);
|
||||||
|
const seedTo = Math.max(seedFrom, parseInt(toEl.value, 10) || 15);
|
||||||
|
const backend = backendEl.value;
|
||||||
|
const seeds = [];
|
||||||
|
for (let s = seedFrom; s <= seedTo; s++) seeds.push(s);
|
||||||
|
const total = seeds.length;
|
||||||
|
|
||||||
|
_cancelled = false;
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
progressEl.style.display = '';
|
||||||
|
runBtn.style.display = 'none';
|
||||||
|
cancelBtn.style.display = '';
|
||||||
|
statusEl.textContent = '';
|
||||||
|
progBar.style.width = '0%';
|
||||||
|
progCount.textContent = `0 / ${total}`;
|
||||||
|
progLabel.textContent = 'Generating…';
|
||||||
|
|
||||||
|
for (let i = 0; i < seeds.length; i++) {
|
||||||
|
if (_cancelled) break;
|
||||||
|
const seed = seeds[i];
|
||||||
|
progLabel.textContent = `Seed ${seed}…`;
|
||||||
|
progCount.textContent = `${i} / ${total}`;
|
||||||
|
progBar.style.width = `${Math.round(i / total * 100)}%`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await fetchTtsPreviewBlob(
|
||||||
|
voiceId, text, 'wav', '', backend, false, { seed }
|
||||||
|
);
|
||||||
|
// Estimate duration from WAV byte length: (bytes - 44 header) / (24000 * 2)
|
||||||
|
const durSec = Math.max(0, (blob.size - 44) / (24000 * 2));
|
||||||
|
_addResultRow(seed, blob, durSec, null, false);
|
||||||
|
} catch (e) {
|
||||||
|
_addResultRow(seed, null, 0, e.message || String(e), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progBar.style.width = '100%';
|
||||||
|
progCount.textContent = `${seeds.length} / ${total}`;
|
||||||
|
progLabel.textContent = _cancelled ? 'Cancelled.' : 'Done — listen and pick your seed.';
|
||||||
|
runBtn.style.display = '';
|
||||||
|
cancelBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
runBtn.addEventListener('click', _runFinder);
|
||||||
|
cancelBtn.addEventListener('click', () => { _cancelled = true; });
|
||||||
|
}
|
||||||
@ -410,6 +410,11 @@ function selectVoice(wrap) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Seed Finder panel ────────────────────────────────────────────────────
|
||||||
|
if (typeof attachSeedFinder === 'function') {
|
||||||
|
attachSeedFinder(voiceId, body);
|
||||||
|
}
|
||||||
|
|
||||||
// Track extracted elements for restoreToRow
|
// Track extracted elements for restoreToRow
|
||||||
wrap._extracted = [
|
wrap._extracted = [
|
||||||
activeEl ? {el: activeEl, target: detailRow} : null,
|
activeEl ? {el: activeEl, target: detailRow} : null,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
's-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser',
|
's-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser',
|
||||||
's-performance', 's-routing', 's-connect', 's-settings',
|
's-reader', 's-performance', 's-routing', 's-connect', 's-settings',
|
||||||
's-llms', 's-conversation',
|
's-llms', 's-conversation',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -110,6 +110,7 @@
|
|||||||
'/static/js/voice-picker.js',
|
'/static/js/voice-picker.js',
|
||||||
'/static/js/benchmark-voice-picker.js',
|
'/static/js/benchmark-voice-picker.js',
|
||||||
'/static/js/voice-inspector.js',
|
'/static/js/voice-inspector.js',
|
||||||
|
'/static/js/seed-finder.js',
|
||||||
'/static/js/voice-sources.js',
|
'/static/js/voice-sources.js',
|
||||||
'/static/js/fishaudio-browser.js',
|
'/static/js/fishaudio-browser.js',
|
||||||
'/static/js/integrations.js',
|
'/static/js/integrations.js',
|
||||||
@ -122,6 +123,9 @@
|
|||||||
'/static/js/stt.js',
|
'/static/js/stt.js',
|
||||||
'/static/js/rehearser-parse.js',
|
'/static/js/rehearser-parse.js',
|
||||||
'/static/js/rehearser.js',
|
'/static/js/rehearser.js',
|
||||||
|
'/static/js/reader.js',
|
||||||
|
'/static/js/audiobook.js',
|
||||||
|
'/static/js/character-sheets.js',
|
||||||
];
|
];
|
||||||
var _useBundle = window.APP_USE_BUNDLE === true || location.search.indexOf('bundle=1') !== -1;
|
var _useBundle = window.APP_USE_BUNDLE === true || location.search.indexOf('bundle=1') !== -1;
|
||||||
var _bundleOk = false;
|
var _bundleOk = false;
|
||||||
|
|||||||
304
static/style.css
304
static/style.css
@ -60,8 +60,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Language picker in the sidebar brand */
|
/* Language picker in the sidebar brand */
|
||||||
.app-lang-picker { margin-top: 8px; font-size: 12px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 7px; background: var(--panel); color: var(--text); font-family: var(--font); cursor: pointer; width: 100%; }
|
.app-lang-toggle {
|
||||||
.app-lang-picker:focus { outline: none; border-color: var(--accent); }
|
margin-left: auto; padding: 2px; border: 1px solid var(--border); border-radius: 5px;
|
||||||
|
background: var(--panel); cursor: pointer; line-height: 0; display: inline-flex;
|
||||||
|
align-items: center; transition: border-color .15s, transform .1s;
|
||||||
|
}
|
||||||
|
.app-lang-toggle:hover { border-color: var(--accent); }
|
||||||
|
.app-lang-toggle:active { transform: scale(.94); }
|
||||||
|
.app-lang-toggle:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37,99,235,.18); }
|
||||||
|
.app-lang-toggle .fi { width: 20px; height: 14px; border-radius: 2px; }
|
||||||
|
|
||||||
/* ── App shell ───────────────────────────────────────────────────────────── */
|
/* ── App shell ───────────────────────────────────────────────────────────── */
|
||||||
#app-shell {
|
#app-shell {
|
||||||
@ -82,6 +89,7 @@ body {
|
|||||||
.sidebar-brand h1 {
|
.sidebar-brand h1 {
|
||||||
font-size: 15px; font-weight: 800; color: var(--accent);
|
font-size: 15px; font-weight: 800; color: var(--accent);
|
||||||
letter-spacing: -0.01em; line-height: 1.2;
|
letter-spacing: -0.01em; line-height: 1.2;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
}
|
}
|
||||||
.sidebar-brand p {
|
.sidebar-brand p {
|
||||||
font-size: 14px; color: var(--subtext); margin-top: 3px;
|
font-size: 14px; color: var(--subtext); margin-top: 3px;
|
||||||
@ -2707,6 +2715,55 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.preview-persona-btn:hover { background: rgba(13,148,136,.12); }
|
.preview-persona-btn:hover { background: rgba(13,148,136,.12); }
|
||||||
.preview-persona-btn:disabled { opacity: .5; cursor: default; }
|
.preview-persona-btn:disabled { opacity: .5; cursor: default; }
|
||||||
|
|
||||||
|
/* ── Seed Finder ─────────────────────────────────────────────────────────── */
|
||||||
|
.seed-finder-group { margin-top: 12px; }
|
||||||
|
.seed-finder-body { padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.seed-finder-config { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.seed-finder-row { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.seed-finder-label { font-size: 12px; font-weight: 600; color: var(--subtext); }
|
||||||
|
.seed-finder-text {
|
||||||
|
width: 100%; font-size: 13px; padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--text);
|
||||||
|
font-family: var(--font); resize: vertical; min-height: 68px;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.seed-finder-text:focus { border-color: var(--accent); outline: none; }
|
||||||
|
.seed-finder-params { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: 14px; }
|
||||||
|
.seed-finder-field { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text); }
|
||||||
|
.seed-finder-field input[type="number"] {
|
||||||
|
width: 64px; padding: 5px 7px; font-size: 13px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--text); text-align: center;
|
||||||
|
}
|
||||||
|
.seed-finder-field select {
|
||||||
|
padding: 5px 8px; font-size: 13px;
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--text);
|
||||||
|
}
|
||||||
|
.seed-finder-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.seed-finder-status { font-size: 12px; color: var(--subtext); }
|
||||||
|
|
||||||
|
.seed-finder-progress { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.seed-finder-progress-head { display: flex; justify-content: space-between; font-size: 12px; color: var(--subtext); }
|
||||||
|
.seed-finder-bar { height: 100%; background: var(--accent); transition: width .2s ease; border-radius: 999px; }
|
||||||
|
|
||||||
|
.seed-finder-results { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.seed-finder-result-row {
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
padding: 7px 10px; border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border); background: var(--surface);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.seed-finder-result-row.sfr-active { border-color: var(--accent); background: rgba(var(--accent-rgb, 124,77,255),.07); }
|
||||||
|
.seed-finder-result-row.seed-finder-result-error { opacity: .6; }
|
||||||
|
.sfr-seed { font-weight: 700; min-width: 58px; color: var(--text); }
|
||||||
|
.sfr-dur { color: var(--subtext); font-size: 12px; min-width: 36px; }
|
||||||
|
.sfr-play-btn { font-size: 12px; padding: 4px 10px; }
|
||||||
|
.sfr-use-btn { font-size: 12px; padding: 4px 10px; }
|
||||||
|
.sfr-saved { font-size: 12px; color: var(--green); font-weight: 600; }
|
||||||
|
.sfr-error { font-size: 12px; color: var(--red); }
|
||||||
|
|
||||||
/* ── Conversation Playground ─────────────────────────────────────────────── */
|
/* ── Conversation Playground ─────────────────────────────────────────────── */
|
||||||
.conv-config-bar { padding: 14px 18px 10px; margin-bottom: 0; }
|
.conv-config-bar { padding: 14px 18px 10px; margin-bottom: 0; }
|
||||||
.conv-config-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; }
|
.conv-config-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; }
|
||||||
@ -3771,3 +3828,246 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Read Aloud reader ─────────────────────────────────────────────────── */
|
||||||
|
.reader-controls {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
|
||||||
|
}
|
||||||
|
.reader-ctl { margin: 0; min-width: 150px; }
|
||||||
|
.reader-ctl-grow { flex: 1 1 200px; }
|
||||||
|
.reader-ctl-sm { min-width: 96px; max-width: 130px; }
|
||||||
|
.reader-ctl-sm input { width: 100%; }
|
||||||
|
.reader-norm-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--subtext); white-space: nowrap; align-self: center; cursor: pointer; }
|
||||||
|
.reader-backend-hint { margin-top: 10px; font-size: 12px; color: var(--subtext); display: flex; align-items: flex-start; gap: 6px; }
|
||||||
|
.reader-backend-hint .mdi { color: var(--accent); flex-shrink: 0; }
|
||||||
|
.reader-backend-hint b { color: var(--text); }
|
||||||
|
.reader-backend-hint.reader-hint-warn { color: var(--text); }
|
||||||
|
.reader-backend-hint:empty { display: none; }
|
||||||
|
.reader-voice-pick { display: flex; gap: 6px; }
|
||||||
|
.reader-voice-pick select { flex: 1; min-width: 0; }
|
||||||
|
.reader-speed-label { color: var(--accent); font-weight: 700; }
|
||||||
|
.reader-ctl input[type=range] { width: 100%; accent-color: var(--accent); }
|
||||||
|
.reader-import-btn { cursor: pointer; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||||
|
|
||||||
|
.reader-dropzone {
|
||||||
|
margin-top: 14px; padding: 30px 16px; border: 2px dashed var(--border);
|
||||||
|
border-radius: 12px; text-align: center; color: var(--subtext);
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 5px;
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.reader-dropzone-icon { font-size: 34px; opacity: .5; }
|
||||||
|
.reader-dropzone.dragover { border-color: var(--accent); background: rgba(37,99,235,.06); color: var(--accent); }
|
||||||
|
.reader-dropzone.dragover .reader-dropzone-icon { opacity: 1; }
|
||||||
|
|
||||||
|
.reader-transport {
|
||||||
|
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
||||||
|
margin-top: 14px; padding: 10px 14px; background: var(--surface);
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.reader-tb-group { display: flex; gap: 4px; }
|
||||||
|
.reader-progress-wrap { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 160px; }
|
||||||
|
.reader-progress-track { flex: 1; height: 6px; border-radius: 4px; background: var(--panel); overflow: hidden; }
|
||||||
|
.reader-progress-fill { height: 100%; width: 0; background: var(--accent); transition: width .25s; }
|
||||||
|
.reader-progress-label { font-size: 12px; color: var(--subtext); white-space: nowrap; min-width: 56px; text-align: right; }
|
||||||
|
.reader-doc-title { font-size: 13px; font-weight: 700; color: var(--text); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Saved-books library */
|
||||||
|
.reader-lib-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
|
.reader-lib-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
|
||||||
|
.reader-book {
|
||||||
|
position: relative; padding: 14px; border: 1px solid var(--border); border-radius: 10px;
|
||||||
|
background: var(--panel); cursor: pointer; transition: border-color .15s, transform .1s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
.reader-book:hover { border-color: var(--accent); box-shadow: 0 4px 14px rgba(0,0,0,.08); transform: translateY(-1px); }
|
||||||
|
.reader-book-icon { font-size: 26px; color: var(--accent); line-height: 1; }
|
||||||
|
.reader-book-title { font-weight: 700; margin-top: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.reader-book-meta { font-size: 11px; color: var(--subtext); margin-top: 2px; }
|
||||||
|
.reader-book-bar { position: relative; height: 6px; border-radius: 4px; background: var(--border); margin-top: 10px; overflow: hidden; }
|
||||||
|
.reader-book-bar-synth { position: absolute; inset: 0 auto 0 0; background: rgba(34,197,94,.45); }
|
||||||
|
.reader-book-bar-read { position: absolute; inset: 0 auto 0 0; background: var(--accent); }
|
||||||
|
.reader-book-stats { display: flex; justify-content: space-between; gap: 6px; font-size: 10.5px; color: var(--subtext); margin-top: 5px; }
|
||||||
|
.reader-book-del {
|
||||||
|
position: absolute; top: 6px; right: 6px; border: none; background: rgba(0,0,0,.04);
|
||||||
|
color: var(--subtext); border-radius: 6px; width: 24px; height: 24px; cursor: pointer; opacity: 0; transition: opacity .15s;
|
||||||
|
}
|
||||||
|
.reader-book:hover .reader-book-del { opacity: 1; }
|
||||||
|
.reader-book-del:hover { color: var(--red, #ef4444); background: rgba(239,68,68,.1); }
|
||||||
|
|
||||||
|
/* Synthesise-ahead bar + range selection */
|
||||||
|
.reader-synthbar {
|
||||||
|
display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
|
||||||
|
margin-top: 10px; padding: 8px 12px; background: var(--surface);
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.reader-page-range { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
.reader-page-inp {
|
||||||
|
width: 56px; padding: 4px 6px; border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; background: var(--panel); color: var(--text); font-family: var(--font);
|
||||||
|
}
|
||||||
|
#reader-select-toggle.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||||
|
.reader-sel-info { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--accent); font-weight: 600; }
|
||||||
|
.reader-sel-clear { border: none; background: none; color: var(--subtext); cursor: pointer; font-size: 13px; padding: 0 2px; }
|
||||||
|
.reader-sel-clear:hover { color: var(--red, #ef4444); }
|
||||||
|
.reader-sel-hint { font-size: 12px; color: var(--subtext); }
|
||||||
|
.reader-sel-hint b { color: var(--accent); }
|
||||||
|
.reader-export { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
#reader-audiobook-btn .mdi { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Audiobook live casting — floating, non-blocking, minimisable panel */
|
||||||
|
.ab-castpanel {
|
||||||
|
position: fixed; right: 18px; bottom: 18px; z-index: 1000;
|
||||||
|
width: min(880px, 96vw); max-height: 82vh; display: flex; flex-direction: column;
|
||||||
|
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0,0,0,.28); overflow: hidden;
|
||||||
|
}
|
||||||
|
.ab-castpanel-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); cursor: default; }
|
||||||
|
.ab-castpanel-head > .mdi { color: var(--accent); font-size: 18px; }
|
||||||
|
.ab-castpanel-title { font-weight: 800; font-size: 14px; }
|
||||||
|
.ab-castpanel-count { font-size: 12px; color: var(--subtext); }
|
||||||
|
.ab-castpanel-btn { border: none; background: none; color: var(--subtext); cursor: pointer; padding: 2px 5px; border-radius: 6px; line-height: 1; }
|
||||||
|
.ab-castpanel-btn:hover { background: var(--panel); color: var(--text); }
|
||||||
|
.ab-castpanel-bar { margin: 0; border-radius: 0; }
|
||||||
|
.ab-castpanel-foot { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
||||||
|
.ab-cv-done { font-size: 13px; font-weight: 700; display: inline-flex; align-items: center; gap: 6px; }
|
||||||
|
.ab-cv-done .mdi { color: #10b981; }
|
||||||
|
/* minimised → just the header chip in the corner */
|
||||||
|
.ab-castpanel.minimized { width: auto; max-width: 320px; }
|
||||||
|
.ab-castpanel.minimized .ab-castpanel-bar,
|
||||||
|
.ab-castpanel.minimized .ab-cv-body,
|
||||||
|
.ab-castpanel.minimized .ab-castpanel-foot { display: none; }
|
||||||
|
.ab-castpanel.minimized .ab-castpanel-head { cursor: pointer; border-bottom: none; }
|
||||||
|
|
||||||
|
.ab-cv-body { display: grid; grid-template-columns: 1fr 220px; gap: 12px; padding: 12px; }
|
||||||
|
.ab-cv-feed { height: 44vh; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; }
|
||||||
|
.ab-cv-row { display: grid; grid-template-columns: 130px 1fr; gap: 8px; font-size: 12.5px; padding: 2px 0; align-items: baseline; }
|
||||||
|
.ab-cv-row.is-narr { opacity: .6; }
|
||||||
|
.ab-cv-spk { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.ab-cv-row.is-narr .ab-cv-spk { color: var(--subtext); font-weight: 600; }
|
||||||
|
.ab-cv-txt { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.ab-cv-note { font-size: 11.5px; color: var(--subtext); font-style: italic; padding: 3px 0 3px 4px; border-left: 2px solid var(--border); }
|
||||||
|
.ab-cv-side { border: 1px solid var(--border); border-radius: 8px; padding: 8px; background: var(--panel); overflow-y: auto; height: 44vh; }
|
||||||
|
.ab-cv-side-head { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); margin-bottom: 6px; }
|
||||||
|
.ab-cv-chars { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.ab-cv-empty { font-size: 12px; color: var(--subtext); font-style: italic; }
|
||||||
|
.ab-chip { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; padding: 3px 8px; border: 1px solid var(--border); border-radius: 20px; }
|
||||||
|
.ab-chip-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--c); flex-shrink: 0; }
|
||||||
|
.ab-chip b { margin-left: auto; color: var(--subtext); font-weight: 700; }
|
||||||
|
.audiobook-narr-note { color: var(--subtext); }
|
||||||
|
@media (max-width: 560px) { .ab-cv-body { grid-template-columns: 1fr; } .ab-cv-side { height: auto; max-height: 22vh; } }
|
||||||
|
|
||||||
|
/* Audiobook casting overlay */
|
||||||
|
.audiobook-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
|
.audiobook-box { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px 24px; width: min(420px, 90vw); box-shadow: 0 12px 40px rgba(0,0,0,.25); }
|
||||||
|
.audiobook-title { font-weight: 800; font-size: 15px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.audiobook-title .mdi { color: var(--accent); font-size: 20px; }
|
||||||
|
.audiobook-msg { color: var(--subtext); font-size: 13px; margin: 10px 0 12px; }
|
||||||
|
.audiobook-box .reader-synth-track { width: 100%; }
|
||||||
|
.audiobook-actions { margin-top: 14px; text-align: right; display: flex; gap: 8px; justify-content: flex-end; }
|
||||||
|
.audiobook-count { font-weight: 500; font-size: 12px; color: var(--subtext); margin-left: auto; }
|
||||||
|
.audiobook-preview-box { width: min(760px, 94vw); }
|
||||||
|
.audiobook-preview-box .audiobook-title { flex-wrap: wrap; }
|
||||||
|
.audiobook-seglist { max-height: 56vh; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px; margin-top: 6px; }
|
||||||
|
.audiobook-seg { display: grid; grid-template-columns: 130px 96px 1fr; gap: 8px; align-items: center; padding: 4px 6px; border-radius: 6px; }
|
||||||
|
.audiobook-seg:nth-child(odd) { background: var(--panel); }
|
||||||
|
.audiobook-seg.is-dialog { box-shadow: inset 3px 0 0 var(--accent); }
|
||||||
|
.audiobook-seg input { padding: 3px 6px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text); font-size: 12px; font-family: var(--font); min-width: 0; }
|
||||||
|
.audiobook-seg input:disabled { opacity: .4; }
|
||||||
|
.audiobook-seg-text { font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
#reh-tb-audiobook .mdi { color: var(--accent); }
|
||||||
|
#reader-charsheets-btn .mdi, #reh-charsheets-btn .mdi { color: var(--accent); }
|
||||||
|
|
||||||
|
/* Character sheets overlay */
|
||||||
|
.cs-box { width: min(900px, 95vw); max-height: 90vh; display: flex; flex-direction: column; }
|
||||||
|
.cs-titlebar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.cs-list { overflow-y: auto; padding-right: 4px; display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 12px; align-content: start; }
|
||||||
|
.cs-group-label { grid-column: 1 / -1; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); margin-top: 4px; }
|
||||||
|
.cs-card { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; background: var(--panel); }
|
||||||
|
.cs-card.cs-main { box-shadow: inset 4px 0 0 var(--accent); }
|
||||||
|
.cs-card.cs-supp { box-shadow: inset 4px 0 0 var(--border); }
|
||||||
|
.cs-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.cs-name { font-weight: 800; font-size: 15px; }
|
||||||
|
.cs-archetype { font-size: 12px; font-weight: 700; color: var(--accent); }
|
||||||
|
.cs-tier { margin-left: auto; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); border: 1px solid var(--border); border-radius: 20px; padding: 1px 8px; }
|
||||||
|
.cs-aliases { font-size: 12px; color: var(--subtext); font-style: italic; margin: 2px 0 6px; }
|
||||||
|
.cs-fields { margin: 0; display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.cs-row { display: grid; grid-template-columns: 120px 1fr; gap: 8px; font-size: 12.5px; }
|
||||||
|
.cs-row dt { font-weight: 700; color: var(--subtext); }
|
||||||
|
.cs-row dd { margin: 0; color: var(--text); }
|
||||||
|
.cs-row ul { margin: 0; padding-left: 16px; }
|
||||||
|
.cs-sources { margin-top: 8px; padding-top: 7px; border-top: 1px dashed var(--border); font-size: 11px; color: var(--subtext); }
|
||||||
|
.cs-sources b { color: var(--text); }
|
||||||
|
.cs-sources .mdi { color: var(--accent); }
|
||||||
|
.reader-synth-prog { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; }
|
||||||
|
.reader-synth-track { width: 130px; height: 6px; border-radius: 4px; background: var(--panel); overflow: hidden; }
|
||||||
|
.reader-synth-fill { height: 100%; width: 0; background: rgba(234,179,8,.9); transition: width .15s; }
|
||||||
|
.reader-synth-label { font-size: 12px; color: var(--subtext); min-width: 52px; }
|
||||||
|
|
||||||
|
/* Select mode: clickable sentences (pointer, not crosshair — it's click-to-pick) */
|
||||||
|
.reader-doc.reader-selecting { box-shadow: inset 0 0 0 2px var(--accent); }
|
||||||
|
.reader-doc.reader-selecting .reader-stat { pointer-events: auto; cursor: pointer; }
|
||||||
|
.reader-doc.reader-selecting .reader-stat:hover { background: rgba(37,99,235,.16); outline: 1px dashed rgba(37,99,235,.5); }
|
||||||
|
.reader-doc.reader-selecting .reader-word { cursor: pointer; }
|
||||||
|
.reader-doc.reader-selecting .reader-word:hover { background: rgba(37,99,235,.18); }
|
||||||
|
/* committed range */
|
||||||
|
.reader-stat.sel { outline: 2px solid var(--accent); outline-offset: 0; background: rgba(37,99,235,.10); }
|
||||||
|
.reader-word.in-sel { box-shadow: inset 0 -2px 0 var(--accent); }
|
||||||
|
/* the start "anchor" pulses while waiting for the end click */
|
||||||
|
.reader-stat.sel-anchor { outline: 2px solid var(--accent); background: rgba(37,99,235,.20); animation: readerAnchorPulse 1s ease-in-out infinite; }
|
||||||
|
.reader-word.sel-anchor { box-shadow: inset 0 -2px 0 var(--accent); background: rgba(37,99,235,.22); animation: readerAnchorPulse 1s ease-in-out infinite; }
|
||||||
|
@keyframes readerAnchorPulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
|
||||||
|
|
||||||
|
/* Zoom toolbar + status legend */
|
||||||
|
.reader-toolbar {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;
|
||||||
|
margin-top: 10px; padding: 8px 12px; background: var(--surface);
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.reader-zoom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.reader-zoom-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
|
||||||
|
.reader-zoom-pct { font-size: 12px; font-weight: 700; min-width: 44px; text-align: center; color: var(--subtext); }
|
||||||
|
.reader-legend { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 11px; color: var(--subtext); }
|
||||||
|
.reader-leg { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
.reader-leg::before { content: ''; width: 11px; height: 11px; border-radius: 3px; }
|
||||||
|
.reader-leg-pending::before { background: rgba(239,68,68,.45); }
|
||||||
|
.reader-leg-synth::before { background: rgba(234,179,8,.65); }
|
||||||
|
.reader-leg-ready::before { background: rgba(34,197,94,.55); }
|
||||||
|
.reader-leg-reading::before { background: rgba(37,99,235,.6); }
|
||||||
|
|
||||||
|
.reader-doc {
|
||||||
|
margin-top: 14px; max-height: 72vh; overflow-y: auto;
|
||||||
|
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
padding: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px;
|
||||||
|
}
|
||||||
|
.reader-doc:empty { display: none; }
|
||||||
|
.reader-parsing { display: flex; align-items: center; gap: 10px; padding: 24px; color: var(--subtext); font-size: 13px; }
|
||||||
|
.reader-doc.two-page { flex-direction: row; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
|
||||||
|
.reader-page {
|
||||||
|
position: relative; flex: 0 0 auto;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,.12); background: #fff;
|
||||||
|
}
|
||||||
|
.reader-canvas { display: block; }
|
||||||
|
.reader-overlay { position: absolute; inset: 0; pointer-events: none; }
|
||||||
|
.reader-hl-word {
|
||||||
|
position: absolute; border-radius: 3px; background: rgba(37,99,235,.30); z-index: 3;
|
||||||
|
box-shadow: 0 0 0 1.5px rgba(37,99,235,.75); transition: left .12s, top .12s, width .12s, height .12s;
|
||||||
|
}
|
||||||
|
/* Per-sentence synthesis-state overlay (PDF) */
|
||||||
|
.reader-stat { position: absolute; border-radius: 3px; z-index: 1; transition: background .2s; }
|
||||||
|
.reader-stat-pending { background: rgba(239,68,68,.12); }
|
||||||
|
.reader-stat-synth { background: rgba(234,179,8,.30); }
|
||||||
|
.reader-stat-ready { background: rgba(34,197,94,.18); }
|
||||||
|
.reader-stat-reading { background: rgba(37,99,235,.20); z-index: 2; }
|
||||||
|
|
||||||
|
.reader-text {
|
||||||
|
max-width: 760px; background: var(--surface); color: var(--text);
|
||||||
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
padding: 28px 34px; line-height: 1.85; font-size: 17px;
|
||||||
|
}
|
||||||
|
.reader-text-para { margin: 0 0 1em; }
|
||||||
|
.reader-word { border-radius: 3px; padding: 0 1px; cursor: pointer; transition: background .12s; }
|
||||||
|
/* Per-sentence synthesis-state tint (text) */
|
||||||
|
.reader-word.stat-pending { background: rgba(239,68,68,.09); }
|
||||||
|
.reader-word.stat-synth { background: rgba(234,179,8,.22); }
|
||||||
|
.reader-word.stat-ready { background: rgba(34,197,94,.14); }
|
||||||
|
.reader-word.stat-reading { background: rgba(37,99,235,.15); }
|
||||||
|
.reader-word.is-word { background: rgba(37,99,235,.42) !important; box-shadow: 0 0 0 1px rgba(37,99,235,.6); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user