migrate to root dir
This commit is contained in:
151
lib/pipeline/pipeline.ts
Normal file
151
lib/pipeline/pipeline.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { loadStoryConfig } from "./config";
|
||||
import { validatePublicDomain } from "./validator";
|
||||
import { sanitizeText } from "./sanitizer";
|
||||
import { chunkText } from "./chunker";
|
||||
import { generateAudio, generateSingleAudio } from "./tts";
|
||||
import { combineAudio, getChunkDuration } from "./audio";
|
||||
import { generateImage } from "./images";
|
||||
import { createVideo } from "./video";
|
||||
import { createSrt } from "./subtitles";
|
||||
import { generateYouTubeMetadata, uploadToYouTube, YouTubeMetadata } from "./uploader";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
export interface RunPipelineOptions {
|
||||
force?: boolean;
|
||||
skipUpload?: boolean;
|
||||
concurrency?: number;
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
export interface RunPipelineResult {
|
||||
storyName: string;
|
||||
audioFiles: string[];
|
||||
imageFiles: string[];
|
||||
srtPath: string;
|
||||
finalAudioPath: string;
|
||||
videoPath: string;
|
||||
metadata: YouTubeMetadata;
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
mapper: (item: T, index: number) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (items.length === 0) return;
|
||||
let nextIndex = 0;
|
||||
const inFlight: Promise<void>[] = [];
|
||||
const launchNext = () => {
|
||||
if (nextIndex >= items.length) return;
|
||||
const current = nextIndex++;
|
||||
const p = mapper(items[current], current).finally(() => {
|
||||
const idx = inFlight.indexOf(p);
|
||||
if (idx >= 0) inFlight.splice(idx, 1);
|
||||
});
|
||||
inFlight.push(p);
|
||||
};
|
||||
for (let i = 0; i < Math.min(limit, items.length); i++) {
|
||||
launchNext();
|
||||
}
|
||||
while (inFlight.length > 0 || nextIndex < items.length) {
|
||||
while (inFlight.length < limit && nextIndex < items.length) {
|
||||
launchNext();
|
||||
}
|
||||
await Promise.race(inFlight);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runStoryPipeline(
|
||||
storyName: string,
|
||||
options: RunPipelineOptions = {}
|
||||
): Promise<RunPipelineResult> {
|
||||
const force = !!options.force;
|
||||
const skipUpload = !!options.skipUpload;
|
||||
const concurrency = Math.max(1, options.concurrency ?? 3);
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
const targetCwd = options.baseDir || originalCwd;
|
||||
|
||||
if (targetCwd && targetCwd !== originalCwd) {
|
||||
process.chdir(targetCwd);
|
||||
}
|
||||
|
||||
try {
|
||||
const storyConfig = loadStoryConfig(storyName);
|
||||
|
||||
const validationResult = validatePublicDomain(storyConfig);
|
||||
if (!validationResult.is_public_domain) {
|
||||
throw new Error(validationResult.message);
|
||||
}
|
||||
|
||||
const storyRoot = path.resolve("stories", storyName);
|
||||
["audio", "images", "final_audio", "video"].forEach((d) =>
|
||||
fs.mkdirSync(path.join(storyRoot, d), { recursive: true })
|
||||
);
|
||||
|
||||
const sanitizedText = sanitizeText(storyName);
|
||||
|
||||
const chunks = chunkText(sanitizedText, storyConfig.config.chunk_size);
|
||||
|
||||
const introFile = path.join("stories", storyName, storyConfig.config.intro_audio_file);
|
||||
const outroFile = path.join("stories", storyName, storyConfig.config.outro_audio_file);
|
||||
if (!fs.existsSync(introFile) || force) {
|
||||
await generateSingleAudio(storyConfig, "This is the intro.", introFile);
|
||||
}
|
||||
if (!fs.existsSync(outroFile) || force) {
|
||||
await generateSingleAudio(storyConfig, "This is the outro.", outroFile);
|
||||
}
|
||||
|
||||
const audioFiles: string[] = new Array(chunks.length);
|
||||
const chunkDurations: number[] = new Array(chunks.length);
|
||||
await mapWithConcurrency(chunks, concurrency, async (chunk, i) => {
|
||||
const audioPath = path.join("stories", storyName, "audio", `chunk_${i}.mp3`);
|
||||
if (!fs.existsSync(audioPath) || force) {
|
||||
await generateAudio(storyConfig, storyName, chunk, i);
|
||||
}
|
||||
const duration = await getChunkDuration(audioPath);
|
||||
audioFiles[i] = audioPath;
|
||||
chunkDurations[i] = duration;
|
||||
});
|
||||
|
||||
const imageFiles: string[] = new Array(chunks.length);
|
||||
await mapWithConcurrency(chunks, concurrency, async (chunk, i) => {
|
||||
const imagePath = path.join("stories", storyName, "images", `chunk_${i}_img0.png`);
|
||||
if (!fs.existsSync(imagePath) || force) {
|
||||
const generated = await generateImage(storyName, storyConfig, chunk, i, 0);
|
||||
imageFiles[i] = generated;
|
||||
} else {
|
||||
imageFiles[i] = imagePath;
|
||||
}
|
||||
});
|
||||
|
||||
const srtPath = createSrt(storyName, chunks, chunkDurations);
|
||||
|
||||
await combineAudio(storyName, storyConfig, audioFiles);
|
||||
const finalAudioPath = path.resolve("stories", storyName, "final_audio", "final.mp3");
|
||||
|
||||
await createVideo(storyName, storyConfig, imageFiles, chunkDurations, srtPath);
|
||||
const videoPath = path.resolve("stories", storyName, "video", "final.mp4");
|
||||
|
||||
const metadata = generateYouTubeMetadata(storyConfig);
|
||||
|
||||
if (!skipUpload) {
|
||||
await uploadToYouTube(videoPath, metadata);
|
||||
}
|
||||
|
||||
return {
|
||||
storyName,
|
||||
audioFiles,
|
||||
imageFiles,
|
||||
srtPath,
|
||||
finalAudioPath,
|
||||
videoPath,
|
||||
metadata,
|
||||
};
|
||||
} finally {
|
||||
if (targetCwd !== originalCwd) {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user