route.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { NextResponse } from "next/server";
  2. import { createJob, updateJob } from "@/lib/job-store";
  3. import { loadConfig } from "@/lib/config";
  4. import { resolveOutputDir } from "@pipeline/shared/node";
  5. import { spawn } from "node:child_process";
  6. import { writeFileSync, mkdirSync, statSync } from "node:fs";
  7. import { join, resolve } from "node:path";
  8. import { tmpdir } from "node:os";
  9. export async function POST(request: Request) {
  10. const body = await request.json();
  11. const { input, options } = body as {
  12. input: {
  13. text?: string;
  14. source?: string;
  15. sourceArgs?: Record<string, string>;
  16. template: string;
  17. platform?: string;
  18. platforms?: string[];
  19. ttsProvider: string;
  20. voiceId?: string;
  21. };
  22. options?: {
  23. noTts?: boolean;
  24. };
  25. };
  26. const platforms: string[] = input.platforms
  27. ?? (input.platform ? input.platform.split(",").map(p => p.trim()) : ["bilibili"]);
  28. if (!input.text && !input.source) {
  29. return NextResponse.json({ error: "text or source is required" }, { status: 400 });
  30. }
  31. const job = createJob({ ...input, text: input.text ?? "", platforms });
  32. const jobDir = join(tmpdir(), "pipeline-jobs", job.id);
  33. mkdirSync(jobDir, { recursive: true });
  34. // Same unified output directory the CLI resolves (OUTPUT_DIR > config > ./output).
  35. const config = loadConfig();
  36. const outputDir = resolveOutputDir(config?.output?.dir);
  37. const cliArgs = [
  38. "render",
  39. "-t", input.template,
  40. "-p", platforms.join(","),
  41. "--tts-provider", input.ttsProvider,
  42. "--output", outputDir,
  43. ];
  44. if (input.source) {
  45. cliArgs.push("--source", input.source);
  46. if (input.sourceArgs?.owner) cliArgs.push("--source-owner", input.sourceArgs.owner);
  47. if (input.sourceArgs?.repo) cliArgs.push("--source-repo", input.sourceArgs.repo);
  48. } else {
  49. const inputFile = join(jobDir, "input.json");
  50. writeFileSync(inputFile, input.text!, "utf-8");
  51. cliArgs.splice(1, 0, inputFile);
  52. }
  53. if (input.voiceId) {
  54. cliArgs.push("--voice", input.voiceId);
  55. }
  56. if (options?.noTts) {
  57. cliArgs.push("--no-tts");
  58. }
  59. const cliPath = resolve(process.cwd(), "../cli/dist/index.js");
  60. const projectRoot = resolve(process.cwd(), "..");
  61. updateJob(job.id, { status: "running" });
  62. const child = spawn("node", [cliPath, ...cliArgs], {
  63. cwd: projectRoot,
  64. stdio: ["ignore", "pipe", "pipe"],
  65. env: { ...process.env },
  66. });
  67. let stdout = "";
  68. let stderr = "";
  69. child.stdout.on("data", (data: Buffer) => {
  70. stdout += data.toString();
  71. });
  72. child.stderr.on("data", (data: Buffer) => {
  73. stderr += data.toString();
  74. });
  75. child.on("close", (code: number) => {
  76. if (code === 0) {
  77. const mp4Matches = [...stdout.matchAll(/-> (.+\.mp4)/g)];
  78. const filePaths = mp4Matches.map(m => m[1].trim());
  79. const outputFiles = filePaths.map(filePath => {
  80. let fileSizeBytes = 0;
  81. try { fileSizeBytes = statSync(filePath).size; } catch {}
  82. const isPortrait = filePath.includes("douyin") || filePath.includes("9x16");
  83. return {
  84. filePath,
  85. fileSizeBytes,
  86. width: isPortrait ? 1080 : 1920,
  87. height: isPortrait ? 1920 : 1080,
  88. durationSeconds: 0,
  89. };
  90. });
  91. const ossUrls = [...stdout.matchAll(/^[ \t]*oss: (\S+)/gm)].map(m => m[1].trim());
  92. const publishWarn = stdout.match(/^Publish warning: (.+)$/m);
  93. const publishError = publishWarn ? publishWarn[1].trim() : undefined;
  94. updateJob(job.id, {
  95. status: "completed",
  96. outputFiles: outputFiles.length > 0 ? outputFiles : undefined,
  97. ossUrls: ossUrls.length > 0 ? ossUrls : undefined,
  98. publishError,
  99. });
  100. } else {
  101. updateJob(job.id, {
  102. status: "failed",
  103. error: stderr.trim() || `Process exited with code ${code}`,
  104. });
  105. }
  106. });
  107. child.on("error", (err: Error) => {
  108. updateJob(job.id, {
  109. status: "failed",
  110. error: err.message,
  111. });
  112. });
  113. return NextResponse.json({ jobId: job.id, status: "started" });
  114. }