Files
UmbraAudio/umbraaudio/project_bootstrap.py
2026-10-18 19:01:04 -07:00

86 lines
2.7 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Reusable library entry point for creating a new, isolated project.
Extracted from the ``umbraaudio init`` CLI path so the web UI can bootstrap a
brand-new project (its own directory + SQLite DB + parsed chapters) without
touching the currently-active project. Mirrors ``cli.init`` exactly.
"""
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Any
from umbraaudio.config import load_config, slugify_project_name
from umbraaudio.database import Database
from umbraaudio.parsers import detect_format, parse_manuscript
from umbraaudio.voices.registry import VoiceRegistry
async def _bootstrap(
manuscript_path: Path,
project_name: str | None = None,
) -> dict[str, Any]:
cfg = load_config(project_name or slugify_project_name(manuscript_path.name))
project_dir: Path = cfg.project_dir
name = project_name or slugify_project_name(manuscript_path.name)
source_format = detect_format(manuscript_path)
chapters = parse_manuscript(manuscript_path, source_format)
db = Database(cfg.db_path)
await db.connect()
project_id: int = await db.create_project(
name=name,
source_path=str(manuscript_path.resolve()),
source_format=source_format,
)
registry = VoiceRegistry(cfg.voice_registry_path)
await registry.connect()
try:
for i, ch in enumerate(chapters):
await db.insert_chapter(
project_id=project_id,
index=i,
title=ch.get("title"),
source_text=ch["text"],
word_count=len(ch["text"].split()),
)
await db.update_project_status(project_id, "parsed")
finally:
await registry.close()
await db.close()
return {
"project_id": project_id,
"slug": project_dir.name,
"name": name,
"project_dir": project_dir,
"data_dir": cfg.data_dir,
"db_path": cfg.db_path,
"chapter_count": len(chapters),
}
def create_project_from_manuscript(
manuscript_path: Path,
project_name: str | None = None,
) -> dict[str, Any]:
"""Create a fresh isolated project from a manuscript path (synchronous).
Returns a dict with ``slug``, ``project_id``, ``name``, paths, and
``chapter_count``.
"""
return asyncio.run(_bootstrap(manuscript_path, project_name))
async def create_project_from_manuscript_async(
manuscript_path: Path,
project_name: str | None = None,
) -> dict[str, Any]:
"""Async variant for use inside a running event loop (e.g. web routes)."""
return await _bootstrap(manuscript_path, project_name)