Files
VoiceStudio/backend/core/onboarding.py
T
a28a19dc63 fix(onboarding): commit + bundle the demo voice clip (#621) (#633)
backend/assets/samples/demo_voice.wav is a build artifact (generated by
scripts/build_demos.sh) that was never committed, so it shipped absent from
installs: onboarding logged 'Demo audio not found', seeded nothing, and the
Launchpad was empty on first run + the /demo_audio route was unavailable.

The file is already un-ignored in .gitignore and bundled via the Tauri
'backend' resource — it just needed to exist in git. Commit it (regenerated
via the script's say/Samantha path, 24kHz mono 16-bit, content matching
DEMO_REF_TEXT) so first-run works on every platform. Onboarding keeps its
graceful skip (now with a regenerate hint) for a partial checkout.

Regression test guards the asset is present + valid and that onboarding seeds
the demo profile from it (and is a no-op on a non-empty DB). No version bump.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:56:07 +05:30

102 lines
3.6 KiB
Python

"""First-run onboarding — seeds a demo voice profile so the Launchpad
isn't empty on initial launch. Runs once; skips silently if any
profiles already exist.
"""
import os
import shutil
import time
import logging
from core.db import get_db
from core.config import VOICES_DIR
logger = logging.getLogger(__name__)
# Bundled demo clip — a short reference audio for the sample profile.
_DEMO_AUDIO = os.path.join(
os.path.dirname(__file__), os.pardir, "assets", "samples", "demo_voice.wav"
)
DEMO_PROFILE_ID = "demo0001"
DEMO_PROFILE_NAME = "OmniVoice Demo Voice"
# Must match the actual spoken content of backend/assets/samples/demo_voice.wav.
# Regenerated by scripts/build_demos.sh — update both files in lockstep.
DEMO_REF_TEXT = (
"Hi, I'm the OmniVoice demo voice. Everything you hear me say from now on "
"was synthesized on your own machine. No cloud, no account, just you and "
"the model."
)
_DEMO_DESCRIPTION = (
"A neutral reference voice bundled with OmniVoice. Clone it to hear how "
"the engine sounds on your machine, then replace it with your own "
"recording when you're ready."
)
def _backfill_demo_metadata(conn):
"""v0.2.x → v0.3.0 upgrade: a user who already had demo0001 seeded
before the alembic migration ran will have description='' and
is_demo=0 on that row. Backfill on every boot — cheap, idempotent."""
try:
conn.execute(
"UPDATE voice_profiles SET description=?, is_demo=1, ref_text=? "
"WHERE id=? AND (is_demo=0 OR description='' OR ref_text!=?)",
(_DEMO_DESCRIPTION, DEMO_REF_TEXT, DEMO_PROFILE_ID, DEMO_REF_TEXT),
)
conn.commit()
except Exception as e:
# Columns may not exist yet if alembic hasn't run — non-fatal.
logger.debug("Demo backfill skipped: %s", e)
def seed_sample_project():
"""Create the demo voice profile if no profiles exist yet."""
conn = get_db()
try:
_backfill_demo_metadata(conn)
count = conn.execute("SELECT COUNT(*) FROM voice_profiles").fetchone()[0]
if count > 0:
return # Not first run — skip
# The demo clip is committed at backend/assets/samples/demo_voice.wav and
# bundled with the app (#621). If it's somehow absent (e.g. a partial
# checkout), skip the seed gracefully rather than seeding a profile that
# points at a missing file — run scripts/build_demos.sh to regenerate it.
if not os.path.isfile(_DEMO_AUDIO):
logger.warning(
"Demo audio not found at %s — skipping onboarding seed "
"(regenerate with scripts/build_demos.sh)", _DEMO_AUDIO,
)
return
# Copy demo audio to voices directory
os.makedirs(VOICES_DIR, exist_ok=True)
dest = os.path.join(VOICES_DIR, f"{DEMO_PROFILE_ID}.wav")
shutil.copy2(_DEMO_AUDIO, dest)
conn.execute(
"INSERT OR IGNORE INTO voice_profiles "
"(id, name, ref_audio_path, ref_text, instruct, language, "
" personality, description, is_demo, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
DEMO_PROFILE_ID,
DEMO_PROFILE_NAME,
f"{DEMO_PROFILE_ID}.wav",
DEMO_REF_TEXT,
"",
"English",
"",
_DEMO_DESCRIPTION,
1,
time.time(),
),
)
conn.commit()
logger.info("🎉 Seeded demo voice profile '%s'", DEMO_PROFILE_NAME)
finally:
conn.close()