Files
project-noctivus/app/page.tsx
2025-08-08 19:26:21 +02:00

140 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
type PipelineResult = {
videoPath: string;
finalAudioPath: string;
imageFiles: string[];
};
type ApiSuccess = { ok: true; result: PipelineResult };
type ApiError = { ok: false; error?: string };
type ApiResponse = ApiSuccess | ApiError;
function isApiResponse(value: unknown): value is ApiResponse {
if (typeof value !== "object" || value === null) return false;
const v = value as Record<string, unknown>;
if (v.ok === true) {
const r = v.result as Record<string, unknown> | undefined;
return (
typeof r === "object" &&
r !== null &&
typeof r.videoPath === "string" &&
typeof r.finalAudioPath === "string" &&
Array.isArray(r.imageFiles)
);
}
return v.ok === false;
}
export default function Home() {
const [storyName, setStoryName] = useState("sample_story");
const [concurrency, setConcurrency] = useState(3);
const [force, setForce] = useState(false);
const [skipUpload, setSkipUpload] = useState(true);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<PipelineResult | null>(null);
const [error, setError] = useState<string | null>(null);
async function runPipeline(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError(null);
setResult(null);
try {
const res = await fetch("/api/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ storyName, concurrency, force, skipUpload }),
});
const data: unknown = await res.json();
if (!res.ok || !isApiResponse(data) || data.ok !== true) {
const msg = isApiResponse(data) && data.ok === false ? data.error : undefined;
throw new Error(msg || `Request failed with status ${res.status}`);
}
setResult(data.result);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
setError(message);
} finally {
setLoading(false);
}
}
return (
<div className="min-h-screen w-full flex items-center justify-center p-8">
<div className="w-full max-w-2xl space-y-6">
<h1 className="text-2xl font-semibold">Audiobooks Hustle Run Pipeline</h1>
<form onSubmit={runPipeline} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Story name</label>
<input
className="w-full rounded-md border px-3 py-2"
placeholder="sample_story"
value={storyName}
onChange={(e) => setStoryName(e.target.value)}
required
/>
<p className="text-xs text-muted-foreground mt-1">
Must match a folder in the repo root at <code>stories/&lt;storyName&gt;</code> containing
<code>source.txt</code> and <code>config.yaml</code>.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Concurrency</label>
<input
type="number"
min={1}
max={10}
className="w-full rounded-md border px-3 py-2"
value={concurrency}
onChange={(e) => setConcurrency(parseInt(e.target.value || "3", 10))}
/>
</div>
<div className="flex items-center gap-4">
<label className="inline-flex items-center gap-2">
<input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />
<span>Force regenerate</span>
</label>
<label className="inline-flex items-center gap-2">
<input type="checkbox" checked={skipUpload} onChange={(e) => setSkipUpload(e.target.checked)} />
<span>Skip upload</span>
</label>
</div>
</div>
<button
type="submit"
className="rounded-md bg-black text-white px-4 py-2 disabled:opacity-50"
disabled={loading}
>
{loading ? "Running..." : "Run pipeline"}
</button>
</form>
{error && <div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">{error}</div>}
{result && (
<div className="space-y-2 text-sm">
<div className="rounded-md border p-3">
<div className="font-medium">Video created:</div>
<pre className="overflow-auto whitespace-pre-wrap">{result.videoPath}</pre>
</div>
<div className="rounded-md border p-3">
<div className="font-medium">Final audio:</div>
<pre className="overflow-auto whitespace-pre-wrap">{result.finalAudioPath}</pre>
</div>
<div className="rounded-md border p-3">
<div className="font-medium">Images ({result.imageFiles?.length ?? 0}):</div>
<pre className="overflow-auto whitespace-pre-wrap">{(result.imageFiles || []).join("\n")}</pre>
</div>
</div>
)}
</div>
</div>
);
}