Initial commit: InkFlow — EPUB vers livre audio local (MLX/Kokoro)

This commit is contained in:
2026-06-21 00:10:11 +02:00
commit d3bb91394b
71 changed files with 8138 additions and 0 deletions

99
frontend/src/BookView.jsx Normal file
View File

@@ -0,0 +1,99 @@
import React, { useEffect, useState } from "react";
import { api, subscribeState } from "./api.js";
import { StatusChip, ProgressBar, Spinner } from "./ui.jsx";
import Chapters from "./Chapters.jsx";
import AnalysisEditor from "./AnalysisEditor.jsx";
import CastEditor from "./CastEditor.jsx";
import PronunciationEditor from "./PronunciationEditor.jsx";
const STAGES = [
{ key: "analyze", label: "Analyse", action: (s) => api.analyze(s), hint: "Découpe le texte, détecte les locuteurs et le casting." },
{ key: "cast", label: "Casting", action: (s) => api.castAuto(s), hint: "Attribue une voix à chaque personnage." },
{ key: "pronounce", label: "Prononciations", action: (s) => api.pronounce(s), hint: "Repère les mots à risque de mauvaise prononciation." },
];
export default function BookView({ slug, onBack }) {
const [data, setData] = useState(null);
const [state, setState] = useState(null);
const [tab, setTab] = useState("chapters");
useEffect(() => {
api.getBook(slug).then((d) => { setData(d); setState(d.state); });
const unsub = subscribeState(slug, setState);
return unsub;
}, [slug]);
if (!data) return <p className="text-ink-muted"><Spinner /> chargement</p>;
const { book } = data;
const st = state || data.state;
const busy = !!st.active_stage;
return (
<div className="space-y-6">
<button onClick={onBack} className="text-sm text-ink-muted hover:text-ink-text"> Bibliothèque</button>
<div className="flex gap-5">
{book.cover_file && (
<img src={api.coverUrl(slug)} alt="" className="h-44 rounded-md border border-ink-edge object-cover" />
)}
<div className="flex-1">
<h1 className="font-serif text-2xl">{book.title}</h1>
<p className="text-ink-muted">{book.author}</p>
<p className="mt-1 text-sm text-ink-muted">{book.chapters.filter((c) => c.render).length} chapitres à narrer</p>
{busy && (
<div className="mt-4 max-w-md space-y-1">
<div className="flex justify-between text-xs text-ink-accent">
<span>{st.active_detail || st.active_stage}</span>
<span>{Math.round((st.active_progress || 0) * 100)}%</span>
</div>
<ProgressBar value={st.active_progress} />
</div>
)}
</div>
</div>
{/* Pipeline */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{STAGES.map((stage) => {
const status = st.stages?.[stage.key] || "pending";
return (
<div key={stage.key} className="card p-4">
<div className="flex items-center justify-between">
<span className="font-medium">{stage.label}</span>
<StatusChip status={status} />
</div>
<p className="mt-1 text-xs text-ink-muted">{stage.hint}</p>
<button className="btn-ghost mt-3" disabled={busy}
onClick={() => stage.action(slug)}>
{status === "done" ? "Relancer" : "Lancer"}
</button>
</div>
);
})}
</div>
{/* Onglets */}
<div className="flex gap-1 border-b border-ink-edge">
{[
["chapters", "Chapitres"],
["analysis", "Analyse"],
["cast", "Casting"],
["pron", "Prononciation"],
].map(([key, label]) => (
<button key={key} onClick={() => setTab(key)}
className={`px-4 py-2 text-sm ${tab === key
? "border-b-2 border-ink-accent text-ink-text"
: "text-ink-muted hover:text-ink-text"}`}>
{label}
</button>
))}
</div>
{tab === "chapters" && <Chapters slug={slug} book={book} state={st} busy={busy} />}
{tab === "analysis" && <AnalysisEditor slug={slug} book={book} state={st} />}
{tab === "cast" && <CastEditor slug={slug} busy={busy} />}
{tab === "pron" && <PronunciationEditor slug={slug} />}
</div>
);
}