nextjs-hackathon-stack 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +4 -9
  2. package/dist/index.js +8 -2
  3. package/package.json +2 -2
  4. package/template/.cursor/rules/ai.mdc +4 -4
  5. package/template/.cursor/rules/general.mdc +2 -2
  6. package/template/.husky/pre-commit +1 -0
  7. package/template/README.md +6 -7
  8. package/template/_env.example +7 -7
  9. package/template/package.json.tmpl +8 -2
  10. package/template/src/app/__tests__/auth-callback.test.ts +72 -0
  11. package/template/src/app/__tests__/error.test.tsx +71 -0
  12. package/template/src/app/__tests__/layout.test.tsx +22 -0
  13. package/template/src/app/__tests__/login-page.test.tsx +34 -0
  14. package/template/src/app/__tests__/protected-layout.test.tsx +43 -0
  15. package/template/src/app/__tests__/protected-page.test.tsx +41 -0
  16. package/template/src/app/api/chat/route.ts +3 -1
  17. package/template/src/features/auth/__tests__/login-form.test.tsx +53 -2
  18. package/template/src/features/auth/__tests__/login.action.test.ts +82 -0
  19. package/template/src/features/auth/__tests__/logout.action.test.ts +32 -0
  20. package/template/src/features/auth/actions/login.action.ts +1 -1
  21. package/template/src/features/auth/components/login-form.tsx +4 -3
  22. package/template/src/features/chat/__tests__/chat-ui.test.tsx +97 -7
  23. package/template/src/features/chat/__tests__/route.test.ts +66 -3
  24. package/template/src/features/chat/__tests__/use-chat.test.ts +15 -0
  25. package/template/src/features/chat/components/chat-ui.tsx +1 -1
  26. package/template/src/shared/__tests__/middleware.test.ts +34 -0
  27. package/template/src/shared/__tests__/schema.test.ts +16 -3
  28. package/template/src/shared/__tests__/supabase-middleware.test.ts +162 -0
  29. package/template/src/shared/__tests__/supabase-server.test.ts +76 -3
  30. package/template/src/shared/__tests__/ui-button.test.tsx +52 -0
  31. package/template/src/shared/__tests__/ui-card.test.tsx +78 -0
  32. package/template/src/shared/__tests__/utils.test.ts +30 -0
  33. package/template/src/shared/lib/ai.ts +2 -2
  34. package/template/vitest.config.ts +5 -0
  35. package/template/src/features/tts/__tests__/route.test.ts +0 -13
  36. package/template/src/features/tts/__tests__/tts-player.test.tsx +0 -21
  37. package/template/src/features/tts/api/route.ts +0 -14
  38. package/template/src/features/tts/components/tts-player.tsx +0 -59
  39. package/template/src/features/video/__tests__/route.test.ts +0 -13
  40. package/template/src/features/video/__tests__/video-generator.test.tsx +0 -21
  41. package/template/src/features/video/api/route.ts +0 -14
  42. package/template/src/features/video/components/video-generator.tsx +0 -56
  43. package/template/src/shared/__tests__/minimax-media.test.ts +0 -58
  44. package/template/src/shared/lib/minimax-media.ts +0 -63
@@ -1,22 +1,95 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
+ const mockSet = vi.fn();
4
+ const mockGetAll = vi.fn().mockReturnValue([]);
5
+ const mockCreateServerClient = vi.fn();
6
+
3
7
  vi.mock("@supabase/ssr", () => ({
4
- createServerClient: vi.fn(() => ({ auth: {} })),
8
+ createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
5
9
  }));
6
10
 
7
11
  vi.mock("next/headers", () => ({
8
12
  cookies: vi.fn(() =>
9
13
  Promise.resolve({
10
- getAll: () => [],
11
- set: vi.fn(),
14
+ getAll: mockGetAll,
15
+ set: mockSet,
12
16
  })
13
17
  ),
14
18
  }));
15
19
 
16
20
  describe("supabase server", () => {
17
21
  it("createClient returns a supabase client", async () => {
22
+ // Arrange
23
+ mockCreateServerClient.mockReturnValue({ auth: {} });
18
24
  const { createClient } = await import("../lib/supabase/server");
25
+
26
+ // Act
19
27
  const client = await createClient();
28
+
29
+ // Assert
20
30
  expect(client).toBeDefined();
21
31
  });
32
+
33
+ it("passes cookie getAll to server client config", async () => {
34
+ // Arrange
35
+ let capturedGetAll: (() => unknown) | undefined;
36
+ mockCreateServerClient.mockImplementation(
37
+ (_u: unknown, _k: unknown, opts: { cookies: { getAll: () => unknown } }) => {
38
+ capturedGetAll = opts.cookies.getAll;
39
+ return { auth: {} };
40
+ }
41
+ );
42
+ const { createClient } = await import("../lib/supabase/server");
43
+
44
+ // Act
45
+ await createClient();
46
+ capturedGetAll!();
47
+
48
+ // Assert
49
+ expect(mockGetAll).toHaveBeenCalled();
50
+ });
51
+
52
+ it("setAll sets cookies via cookieStore", async () => {
53
+ // Arrange
54
+ let capturedSetAll:
55
+ | ((cookies: { name: string; value: string; options: object }[]) => void)
56
+ | undefined;
57
+ mockCreateServerClient.mockImplementation(
58
+ (_u: unknown, _k: unknown, opts: { cookies: { setAll: (c: { name: string; value: string; options: object }[]) => void } }) => {
59
+ capturedSetAll = opts.cookies.setAll;
60
+ return { auth: {} };
61
+ }
62
+ );
63
+ const { createClient } = await import("../lib/supabase/server");
64
+ await createClient();
65
+
66
+ // Act
67
+ capturedSetAll!([{ name: "token", value: "abc", options: { path: "/" } }]);
68
+
69
+ // Assert
70
+ expect(mockSet).toHaveBeenCalledWith("token", "abc", { path: "/" });
71
+ });
72
+
73
+ it("setAll silently swallows errors from server components", async () => {
74
+ // Arrange
75
+ mockSet.mockImplementation(() => {
76
+ throw new Error("Cannot set cookie");
77
+ });
78
+ let capturedSetAll:
79
+ | ((cookies: { name: string; value: string; options: object }[]) => void)
80
+ | undefined;
81
+ mockCreateServerClient.mockImplementation(
82
+ (_u: unknown, _k: unknown, opts: { cookies: { setAll: (c: { name: string; value: string; options: object }[]) => void } }) => {
83
+ capturedSetAll = opts.cookies.setAll;
84
+ return { auth: {} };
85
+ }
86
+ );
87
+ const { createClient } = await import("../lib/supabase/server");
88
+ await createClient();
89
+
90
+ // Act + Assert
91
+ expect(() =>
92
+ capturedSetAll!([{ name: "token", value: "abc", options: {} }])
93
+ ).not.toThrow();
94
+ });
22
95
  });
@@ -0,0 +1,52 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import { Button } from "../components/ui/button";
5
+
6
+ describe("Button", () => {
7
+ it("renders with default variant", () => {
8
+ // Arrange + Act
9
+ render(<Button>Click me</Button>);
10
+
11
+ // Assert
12
+ expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
13
+ });
14
+
15
+ it("renders as disabled when disabled prop is set", () => {
16
+ // Arrange + Act
17
+ render(<Button disabled>Submit</Button>);
18
+
19
+ // Assert
20
+ expect(screen.getByRole("button")).toBeDisabled();
21
+ });
22
+
23
+ it("renders with custom className", () => {
24
+ // Arrange + Act
25
+ render(<Button className="custom-class">Click</Button>);
26
+
27
+ // Assert
28
+ expect(screen.getByRole("button")).toHaveClass("custom-class");
29
+ });
30
+
31
+ it("renders as a Slot (asChild) when asChild is true", () => {
32
+ // Arrange + Act
33
+ render(
34
+ <Button asChild>
35
+ <a href="/home">Go home</a>
36
+ </Button>
37
+ );
38
+
39
+ // Assert
40
+ expect(screen.getByRole("link", { name: /go home/i })).toBeInTheDocument();
41
+ });
42
+
43
+ it("renders with destructive variant", () => {
44
+ // Arrange + Act
45
+ render(<Button variant="destructive">Delete</Button>);
46
+
47
+ // Assert
48
+ const btn = screen.getByRole("button", { name: /delete/i });
49
+ expect(btn).toBeInTheDocument();
50
+ expect(btn.className).toContain("destructive");
51
+ });
52
+ });
@@ -0,0 +1,78 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import {
5
+ Card,
6
+ CardAction,
7
+ CardContent,
8
+ CardDescription,
9
+ CardFooter,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "../components/ui/card";
13
+
14
+ describe("Card components", () => {
15
+ it("renders Card with children", () => {
16
+ // Arrange + Act
17
+ render(<Card data-testid="card">content</Card>);
18
+
19
+ // Assert
20
+ expect(screen.getByTestId("card")).toBeInTheDocument();
21
+ });
22
+
23
+ it("renders CardHeader", () => {
24
+ // Arrange + Act
25
+ render(<CardHeader data-testid="header">header</CardHeader>);
26
+
27
+ // Assert
28
+ expect(screen.getByTestId("header")).toBeInTheDocument();
29
+ });
30
+
31
+ it("renders CardTitle", () => {
32
+ // Arrange + Act
33
+ render(<CardTitle>My Title</CardTitle>);
34
+
35
+ // Assert
36
+ expect(screen.getByText("My Title")).toBeInTheDocument();
37
+ });
38
+
39
+ it("renders CardDescription", () => {
40
+ // Arrange + Act
41
+ render(<CardDescription>My Description</CardDescription>);
42
+
43
+ // Assert
44
+ expect(screen.getByText("My Description")).toBeInTheDocument();
45
+ });
46
+
47
+ it("renders CardContent", () => {
48
+ // Arrange + Act
49
+ render(<CardContent data-testid="content">body</CardContent>);
50
+
51
+ // Assert
52
+ expect(screen.getByTestId("content")).toBeInTheDocument();
53
+ });
54
+
55
+ it("renders CardAction", () => {
56
+ // Arrange + Act
57
+ render(<CardAction data-testid="action">action</CardAction>);
58
+
59
+ // Assert
60
+ expect(screen.getByTestId("action")).toBeInTheDocument();
61
+ });
62
+
63
+ it("renders CardFooter", () => {
64
+ // Arrange + Act
65
+ render(<CardFooter data-testid="footer">footer</CardFooter>);
66
+
67
+ // Assert
68
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
69
+ });
70
+
71
+ it("applies custom className to Card", () => {
72
+ // Arrange + Act
73
+ render(<Card className="custom-class" data-testid="card">content</Card>);
74
+
75
+ // Assert
76
+ expect(screen.getByTestId("card")).toHaveClass("custom-class");
77
+ });
78
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ describe("cn", () => {
6
+ it("merges multiple class names", () => {
7
+ // Arrange + Act + Assert
8
+ expect(cn("foo", "bar")).toBe("foo bar");
9
+ });
10
+
11
+ it("omits falsy conditional classes", () => {
12
+ // Arrange + Act + Assert
13
+ expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
14
+ });
15
+
16
+ it("handles undefined values", () => {
17
+ // Arrange + Act + Assert
18
+ expect(cn("foo", undefined, "bar")).toBe("foo bar");
19
+ });
20
+
21
+ it("deduplicates conflicting tailwind classes (last one wins)", () => {
22
+ // Arrange + Act + Assert
23
+ expect(cn("p-2", "p-4")).toBe("p-4");
24
+ });
25
+
26
+ it("handles empty call", () => {
27
+ // Arrange + Act + Assert
28
+ expect(cn()).toBe("");
29
+ });
30
+ });
@@ -1,8 +1,8 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
 
3
3
  const provider = createOpenAI({
4
- baseURL: process.env.AI_BASE_URL ?? "https://api.minimaxi.chat/v1",
4
+ baseURL: process.env.AI_GATEWAY_URL ?? "https://gateway.ai.vercel.app/v1",
5
5
  apiKey: process.env.AI_API_KEY ?? "",
6
6
  });
7
7
 
8
- export const aiModel = provider(process.env.AI_MODEL ?? "MiniMax-M1");
8
+ export const aiModel = provider(process.env.AI_MODEL ?? "google/gemini-2.0-flash");
@@ -5,8 +5,10 @@ import { resolve } from "path";
5
5
  export default defineConfig({
6
6
  plugins: [react()],
7
7
  test: {
8
+ globals: true,
8
9
  environment: "jsdom",
9
10
  setupFiles: ["./src/test-setup.ts"],
11
+ exclude: ["node_modules/**", "src/e2e/**"],
10
12
  coverage: {
11
13
  provider: "v8",
12
14
  thresholds: {
@@ -22,6 +24,9 @@ export default defineConfig({
22
24
  "**/*.config.*",
23
25
  "**/index.ts",
24
26
  "src/app/globals.css",
27
+ ".next/**",
28
+ "next-env.d.ts",
29
+ "**/*.d.ts",
25
30
  ],
26
31
  },
27
32
  },
@@ -1,13 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@/shared/lib/minimax-media", () => ({
4
- synthesizeSpeech: vi.fn().mockResolvedValue("https://example.com/audio.mp3"),
5
- }));
6
-
7
- describe("tts route", () => {
8
- it("module is importable", async () => {
9
- const mod = await import("../api/route");
10
- expect(mod.runtime).toBe("edge");
11
- expect(typeof mod.POST).toBe("function");
12
- });
13
- });
@@ -1,21 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, it, expect } from "vitest";
3
-
4
- import { TtsPlayer } from "../components/tts-player";
5
-
6
- describe("TtsPlayer", () => {
7
- it("renders text input", () => {
8
- render(<TtsPlayer />);
9
- expect(screen.getByPlaceholderText(/enter text to speak/i)).toBeInTheDocument();
10
- });
11
-
12
- it("renders speak button", () => {
13
- render(<TtsPlayer />);
14
- expect(screen.getByRole("button", { name: /speak/i })).toBeInTheDocument();
15
- });
16
-
17
- it("button disabled when input empty", () => {
18
- render(<TtsPlayer />);
19
- expect(screen.getByRole("button", { name: /speak/i })).toBeDisabled();
20
- });
21
- });
@@ -1,14 +0,0 @@
1
- import { synthesizeSpeech } from "@/shared/lib/minimax-media";
2
-
3
- export const runtime = "edge";
4
-
5
- export async function POST(req: Request) {
6
- const { text, voiceId } = (await req.json()) as { text: string; voiceId?: string };
7
-
8
- if (!text || typeof text !== "string") {
9
- return Response.json({ error: "text is required" }, { status: 400 });
10
- }
11
-
12
- const audioFile = await synthesizeSpeech(text, voiceId);
13
- return Response.json({ audioFile });
14
- }
@@ -1,59 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
-
5
- export function TtsPlayer() {
6
- const [text, setText] = useState("");
7
- const [audioUrl, setAudioUrl] = useState<string | null>(null);
8
- const [isLoading, setIsLoading] = useState(false);
9
- const [error, setError] = useState<string | null>(null);
10
-
11
- const handleSubmit = async (e: React.SyntheticEvent) => {
12
- e.preventDefault();
13
- setIsLoading(true);
14
- setError(null);
15
-
16
- const response = await fetch("/features/tts/api", {
17
- method: "POST",
18
- headers: { "Content-Type": "application/json" },
19
- body: JSON.stringify({ text }),
20
- });
21
-
22
- if (!response.ok) {
23
- setError("Failed to synthesize speech");
24
- setIsLoading(false);
25
- return;
26
- }
27
-
28
- const data = (await response.json()) as { audioFile: string };
29
- setAudioUrl(data.audioFile);
30
- setIsLoading(false);
31
- };
32
-
33
- return (
34
- <div className="space-y-4" data-testid="tts-player">
35
- <form onSubmit={(e) => { void handleSubmit(e); }} className="flex gap-2">
36
- <textarea
37
- value={text}
38
- onChange={(e) => { setText(e.target.value); }}
39
- placeholder="Enter text to speak..."
40
- className="flex-1 rounded border px-3 py-2"
41
- rows={3}
42
- />
43
- <button
44
- type="submit"
45
- disabled={isLoading || !text.trim()}
46
- className="rounded bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
47
- >
48
- {isLoading ? "Synthesizing..." : "Speak"}
49
- </button>
50
- </form>
51
- {error && <p className="text-sm text-red-600">{error}</p>}
52
- {audioUrl && (
53
- <audio controls src={audioUrl} className="w-full">
54
- <track kind="captions" />
55
- </audio>
56
- )}
57
- </div>
58
- );
59
- }
@@ -1,13 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@/shared/lib/minimax-media", () => ({
4
- generateVideo: vi.fn().mockResolvedValue("task-abc"),
5
- }));
6
-
7
- describe("video route", () => {
8
- it("module is importable", async () => {
9
- const mod = await import("../api/route");
10
- expect(mod.runtime).toBe("edge");
11
- expect(typeof mod.POST).toBe("function");
12
- });
13
- });
@@ -1,21 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, it, expect } from "vitest";
3
-
4
- import { VideoGenerator } from "../components/video-generator";
5
-
6
- describe("VideoGenerator", () => {
7
- it("renders prompt input", () => {
8
- render(<VideoGenerator />);
9
- expect(screen.getByPlaceholderText(/describe a video/i)).toBeInTheDocument();
10
- });
11
-
12
- it("renders generate button", () => {
13
- render(<VideoGenerator />);
14
- expect(screen.getByRole("button", { name: /generate/i })).toBeInTheDocument();
15
- });
16
-
17
- it("button disabled when input empty", () => {
18
- render(<VideoGenerator />);
19
- expect(screen.getByRole("button", { name: /generate/i })).toBeDisabled();
20
- });
21
- });
@@ -1,14 +0,0 @@
1
- import { generateVideo } from "@/shared/lib/minimax-media";
2
-
3
- export const runtime = "edge";
4
-
5
- export async function POST(req: Request) {
6
- const { prompt } = (await req.json()) as { prompt: string };
7
-
8
- if (!prompt || typeof prompt !== "string") {
9
- return Response.json({ error: "prompt is required" }, { status: 400 });
10
- }
11
-
12
- const taskId = await generateVideo(prompt);
13
- return Response.json({ taskId });
14
- }
@@ -1,56 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
-
5
- export function VideoGenerator() {
6
- const [prompt, setPrompt] = useState("");
7
- const [taskId, setTaskId] = useState<string | null>(null);
8
- const [isLoading, setIsLoading] = useState(false);
9
- const [error, setError] = useState<string | null>(null);
10
-
11
- const handleSubmit = async (e: React.SyntheticEvent) => {
12
- e.preventDefault();
13
- setIsLoading(true);
14
- setError(null);
15
-
16
- const response = await fetch("/features/video/api", {
17
- method: "POST",
18
- headers: { "Content-Type": "application/json" },
19
- body: JSON.stringify({ prompt }),
20
- });
21
-
22
- if (!response.ok) {
23
- setError("Failed to generate video");
24
- setIsLoading(false);
25
- return;
26
- }
27
-
28
- const data = (await response.json()) as { taskId: string };
29
- setTaskId(data.taskId);
30
- setIsLoading(false);
31
- };
32
-
33
- return (
34
- <div className="space-y-4" data-testid="video-generator">
35
- <form onSubmit={(e) => { void handleSubmit(e); }} className="flex gap-2">
36
- <input
37
- value={prompt}
38
- onChange={(e) => { setPrompt(e.target.value); }}
39
- placeholder="Describe a video..."
40
- className="flex-1 rounded border px-3 py-2"
41
- />
42
- <button
43
- type="submit"
44
- disabled={isLoading || !prompt.trim()}
45
- className="rounded bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
46
- >
47
- {isLoading ? "Generating..." : "Generate"}
48
- </button>
49
- </form>
50
- {error && <p className="text-sm text-red-600">{error}</p>}
51
- {taskId && (
52
- <p className="text-sm text-muted-foreground">Task ID: {taskId}</p>
53
- )}
54
- </div>
55
- );
56
- }
@@ -1,58 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- import { generateVideo, synthesizeSpeech } from "../lib/minimax-media";
4
-
5
- const mockFetch = vi.fn();
6
- global.fetch = mockFetch;
7
-
8
- describe("generateVideo", () => {
9
- beforeEach(() => {
10
- vi.clearAllMocks();
11
- });
12
-
13
- it("returns task_id when API responds successfully", async () => {
14
- // Arrange
15
- mockFetch.mockResolvedValue({
16
- ok: true,
17
- json: () => Promise.resolve({ task_id: "abc123" }),
18
- });
19
-
20
- // Act
21
- const taskId = await generateVideo("A cat on the moon");
22
-
23
- // Assert
24
- expect(taskId).toBe("abc123");
25
- });
26
-
27
- it("throws when API returns non-ok response", async () => {
28
- // Arrange
29
- mockFetch.mockResolvedValue({ ok: false, statusText: "Bad Request" });
30
-
31
- // Act + Assert
32
- await expect(generateVideo("test")).rejects.toThrow("Video generation failed");
33
- });
34
- });
35
-
36
- describe("synthesizeSpeech", () => {
37
- it("returns audio_file URL when API responds successfully", async () => {
38
- // Arrange
39
- mockFetch.mockResolvedValue({
40
- ok: true,
41
- json: () => Promise.resolve({ audio_file: "https://example.com/audio.mp3" }),
42
- });
43
-
44
- // Act
45
- const url = await synthesizeSpeech("Hello world");
46
-
47
- // Assert
48
- expect(url).toBe("https://example.com/audio.mp3");
49
- });
50
-
51
- it("throws when API returns non-ok response", async () => {
52
- // Arrange
53
- mockFetch.mockResolvedValue({ ok: false, statusText: "Server Error" });
54
-
55
- // Act + Assert
56
- await expect(synthesizeSpeech("test")).rejects.toThrow("TTS failed");
57
- });
58
- });
@@ -1,63 +0,0 @@
1
- const MINIMAX_API_BASE = "https://api.minimaxi.chat/v1";
2
-
3
- interface VideoGenerationRequest {
4
- model: string;
5
- prompt: string;
6
- }
7
-
8
- interface VideoGenerationResponse {
9
- task_id: string;
10
- }
11
-
12
- interface TtsRequest {
13
- model: string;
14
- text: string;
15
- voice_id: string;
16
- }
17
-
18
- interface TtsResponse {
19
- audio_file: string;
20
- }
21
-
22
- export async function generateVideo(prompt: string): Promise<string> {
23
- const response = await fetch(`${MINIMAX_API_BASE}/video_generation`, {
24
- method: "POST",
25
- headers: {
26
- Authorization: `Bearer ${process.env.MINIMAX_API_KEY ?? ""}`,
27
- "Content-Type": "application/json",
28
- },
29
- body: JSON.stringify({
30
- model: "video-01",
31
- prompt,
32
- } satisfies VideoGenerationRequest),
33
- });
34
-
35
- if (!response.ok) {
36
- throw new Error(`Video generation failed: ${response.statusText}`);
37
- }
38
-
39
- const data = (await response.json()) as VideoGenerationResponse;
40
- return data.task_id;
41
- }
42
-
43
- export async function synthesizeSpeech(text: string, voiceId = "female-shaonv"): Promise<string> {
44
- const response = await fetch(`${MINIMAX_API_BASE}/t2a_v2`, {
45
- method: "POST",
46
- headers: {
47
- Authorization: `Bearer ${process.env.MINIMAX_API_KEY ?? ""}`,
48
- "Content-Type": "application/json",
49
- },
50
- body: JSON.stringify({
51
- model: "speech-02-hd",
52
- text,
53
- voice_id: voiceId,
54
- } satisfies TtsRequest),
55
- });
56
-
57
- if (!response.ok) {
58
- throw new Error(`TTS failed: ${response.statusText}`);
59
- }
60
-
61
- const data = (await response.json()) as TtsResponse;
62
- return data.audio_file;
63
- }