Spaces:
Paused
Paused
Commit
·
b162b24
0
Parent(s):
music generation
Browse files- .eslintrc.json +3 -0
- .gitignore +37 -0
- Dockerfile +25 -0
- README.md +14 -0
- app/api/generate/cover/route.ts +44 -0
- app/api/generate/title/route.ts +49 -0
- app/favicon.ico +0 -0
- app/layout.tsx +22 -0
- app/page.tsx +19 -0
- assets/globals.css +33 -0
- components/form.tsx +234 -0
- components/hooks/useGeneration.ts +83 -0
- components/length.tsx +69 -0
- components/moods.tsx +66 -0
- components/prompt.tsx +23 -0
- components/styles.tsx +67 -0
- next.config.mjs +15 -0
- package-lock.json +0 -0
- package.json +30 -0
- postcss.config.mjs +8 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- tailwind.config.ts +21 -0
- tsconfig.json +26 -0
- utils/index.ts +252 -0
.eslintrc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals"
|
3 |
+
}
|
.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
.yarn/install-state.gz
|
8 |
+
|
9 |
+
# testing
|
10 |
+
/coverage
|
11 |
+
|
12 |
+
# next.js
|
13 |
+
/.next/
|
14 |
+
/out/
|
15 |
+
.env
|
16 |
+
|
17 |
+
# production
|
18 |
+
/build
|
19 |
+
|
20 |
+
# misc
|
21 |
+
.DS_Store
|
22 |
+
*.pem
|
23 |
+
|
24 |
+
# debug
|
25 |
+
npm-debug.log*
|
26 |
+
yarn-debug.log*
|
27 |
+
yarn-error.log*
|
28 |
+
|
29 |
+
# local env files
|
30 |
+
.env*.local
|
31 |
+
|
32 |
+
# vercel
|
33 |
+
.vercel
|
34 |
+
|
35 |
+
# typescript
|
36 |
+
*.tsbuildinfo
|
37 |
+
next-env.d.ts
|
Dockerfile
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dockerfile
|
2 |
+
|
3 |
+
# Use an official Node.js runtime as the base image
|
4 |
+
FROM node:18
|
5 |
+
|
6 |
+
# Set the working directory in the container
|
7 |
+
WORKDIR /usr/src/app
|
8 |
+
|
9 |
+
# Copy package.json and package-lock.json to the container
|
10 |
+
COPY package.json package-lock.json ./
|
11 |
+
|
12 |
+
# Install dependencies
|
13 |
+
RUN npm install
|
14 |
+
|
15 |
+
# Copy the rest of the application files to the container
|
16 |
+
COPY . .
|
17 |
+
|
18 |
+
# Build the Next.js application for production
|
19 |
+
RUN npm run build
|
20 |
+
|
21 |
+
# Expose the application port (assuming your app runs on port 3000)
|
22 |
+
EXPOSE 3002
|
23 |
+
|
24 |
+
# Start the application
|
25 |
+
CMD ["npm", "start"]
|
README.md
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: — AI Jukebox —
|
3 |
+
header: mini
|
4 |
+
short_description: Generate music powered by AI
|
5 |
+
emoji: 🎶
|
6 |
+
colorFrom: purple
|
7 |
+
colorTo: pink
|
8 |
+
sdk: docker
|
9 |
+
app_port: 3002
|
10 |
+
pinned: false
|
11 |
+
license: mit
|
12 |
+
---
|
13 |
+
|
14 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app/api/generate/cover/route.ts
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest } from "next/server";
|
2 |
+
|
3 |
+
export async function POST(
|
4 |
+
request: NextRequest,
|
5 |
+
) {
|
6 |
+
const { prompt } = await request.json();
|
7 |
+
|
8 |
+
const response = await fetch("https://api-inference.huggingface.co/models/CiroN2022/cd-md-music", {
|
9 |
+
method: "POST",
|
10 |
+
headers: {
|
11 |
+
Authorization: `Bearer ${process.env.HF_TOKEN}`,
|
12 |
+
'Content-Type': 'application/json',
|
13 |
+
['x-use-cache']: "0"
|
14 |
+
},
|
15 |
+
body: JSON.stringify({
|
16 |
+
inputs: prompt,
|
17 |
+
}),
|
18 |
+
})
|
19 |
+
.then((response: any) => {
|
20 |
+
if (response.status !== 200) return Response.json({ status: 500, ok: false, message: response.statusText })
|
21 |
+
|
22 |
+
return response.arrayBuffer()
|
23 |
+
})
|
24 |
+
.then((response) => {
|
25 |
+
return Buffer.from(response)
|
26 |
+
})
|
27 |
+
.catch((error) => {
|
28 |
+
return {
|
29 |
+
error: error.message,
|
30 |
+
}
|
31 |
+
})
|
32 |
+
|
33 |
+
if ("error" in response) {
|
34 |
+
return Response.json({ status: 500, ok: false, message: response.error })
|
35 |
+
}
|
36 |
+
|
37 |
+
// buffer to blob
|
38 |
+
const image = Buffer.from(response).toString('base64')
|
39 |
+
|
40 |
+
return Response.json({
|
41 |
+
image: "data:image/png;base64," + image,
|
42 |
+
})
|
43 |
+
|
44 |
+
}
|
app/api/generate/title/route.ts
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest } from "next/server";
|
2 |
+
|
3 |
+
export async function POST(
|
4 |
+
request: NextRequest,
|
5 |
+
) {
|
6 |
+
const { prompt } = await request.json();
|
7 |
+
|
8 |
+
const response = await fetch("https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-orpo-141b-A35b-v0.1", {
|
9 |
+
method: "POST",
|
10 |
+
headers: {
|
11 |
+
Authorization: `Bearer ${process.env.HF_TOKEN}`,
|
12 |
+
'Content-Type': 'application/json',
|
13 |
+
['x-use-cache']: "0"
|
14 |
+
},
|
15 |
+
body: JSON.stringify({
|
16 |
+
inputs: `Generate and return only a music title based on the following prompt: ${prompt}`,
|
17 |
+
parameters: {
|
18 |
+
num_return_sequences: 1,
|
19 |
+
return_full_text: false
|
20 |
+
}
|
21 |
+
}),
|
22 |
+
})
|
23 |
+
.then((response: any) => {
|
24 |
+
if (response.status !== 200) return Response.json({ status: 500, ok: false, message: response.statusText })
|
25 |
+
|
26 |
+
return response.json()
|
27 |
+
})
|
28 |
+
.then((response) => {
|
29 |
+
let title = response?.[0]?.generated_text;
|
30 |
+
title = title?.substring(title.indexOf('"') + 1, title.lastIndexOf('"'))
|
31 |
+
return {
|
32 |
+
title: title,
|
33 |
+
}
|
34 |
+
})
|
35 |
+
.catch((error) => {
|
36 |
+
return {
|
37 |
+
error: error.message,
|
38 |
+
}
|
39 |
+
})
|
40 |
+
|
41 |
+
if ("error" in response) {
|
42 |
+
return Response.json({ status: 500, ok: false, message: response.error })
|
43 |
+
}
|
44 |
+
|
45 |
+
return Response.json({
|
46 |
+
title: response?.title,
|
47 |
+
})
|
48 |
+
|
49 |
+
}
|
app/favicon.ico
ADDED
|
app/layout.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Metadata } from "next";
|
2 |
+
import { Inter } from "next/font/google";
|
3 |
+
import "@/assets/globals.css";
|
4 |
+
|
5 |
+
const inter = Inter({ subsets: ["latin"] });
|
6 |
+
|
7 |
+
export const metadata: Metadata = {
|
8 |
+
title: "Create Next App",
|
9 |
+
description: "Generated by create next app",
|
10 |
+
};
|
11 |
+
|
12 |
+
export default function RootLayout({
|
13 |
+
children,
|
14 |
+
}: Readonly<{
|
15 |
+
children: React.ReactNode;
|
16 |
+
}>) {
|
17 |
+
return (
|
18 |
+
<html lang="en">
|
19 |
+
<body className={inter.className}>{children}</body>
|
20 |
+
</html>
|
21 |
+
);
|
22 |
+
}
|
app/page.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Form } from "@/components/form";
|
2 |
+
import Image from "next/image";
|
3 |
+
|
4 |
+
export default function Home() {
|
5 |
+
return (
|
6 |
+
<section className="min-h-screen p-12 lg:p-24 bg-stone-950">
|
7 |
+
<Form>
|
8 |
+
<header>
|
9 |
+
<h1 className="text-white font-bold text-3xl">
|
10 |
+
Start making music with AI
|
11 |
+
</h1>
|
12 |
+
<h2 className="text-white/70 font-medium mt-2 text-lg">
|
13 |
+
In-browser text-to-music generation
|
14 |
+
</h2>
|
15 |
+
</header>
|
16 |
+
</Form>
|
17 |
+
</section>
|
18 |
+
);
|
19 |
+
}
|
assets/globals.css
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
:root {
|
6 |
+
--foreground-rgb: 0, 0, 0;
|
7 |
+
--background-start-rgb: 214, 219, 220;
|
8 |
+
--background-end-rgb: 255, 255, 255;
|
9 |
+
}
|
10 |
+
|
11 |
+
@media (prefers-color-scheme: dark) {
|
12 |
+
:root {
|
13 |
+
--foreground-rgb: 255, 255, 255;
|
14 |
+
--background-start-rgb: 0, 0, 0;
|
15 |
+
--background-end-rgb: 0, 0, 0;
|
16 |
+
}
|
17 |
+
}
|
18 |
+
|
19 |
+
body {
|
20 |
+
color: rgb(var(--foreground-rgb));
|
21 |
+
background: linear-gradient(
|
22 |
+
to bottom,
|
23 |
+
transparent,
|
24 |
+
rgb(var(--background-end-rgb))
|
25 |
+
)
|
26 |
+
rgb(var(--background-start-rgb));
|
27 |
+
}
|
28 |
+
|
29 |
+
@layer utilities {
|
30 |
+
.text-balance {
|
31 |
+
text-wrap: balance;
|
32 |
+
}
|
33 |
+
}
|
components/form.tsx
ADDED
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
import Image from "next/image";
|
3 |
+
import {
|
4 |
+
AutoTokenizer,
|
5 |
+
MusicgenForConditionalGeneration,
|
6 |
+
BaseStreamer,
|
7 |
+
} from "@xenova/transformers";
|
8 |
+
|
9 |
+
import { Prompt } from "@/components/prompt";
|
10 |
+
import { Length } from "@/components/length";
|
11 |
+
import { Styles } from "@/components/styles";
|
12 |
+
import { Moods } from "@/components/moods";
|
13 |
+
import { useGeneration } from "@/components/hooks/useGeneration";
|
14 |
+
import { useState, useRef, useEffect } from "react";
|
15 |
+
import { encodeWAV, MODEL_ID } from "@/utils";
|
16 |
+
import classNames from "classnames";
|
17 |
+
|
18 |
+
class CallbackStreamer extends BaseStreamer {
|
19 |
+
[x: string]: any;
|
20 |
+
constructor(callback_fn: any) {
|
21 |
+
super();
|
22 |
+
|
23 |
+
this.callback_fn = callback_fn;
|
24 |
+
}
|
25 |
+
|
26 |
+
put(value: any) {
|
27 |
+
return this.callback_fn(value);
|
28 |
+
}
|
29 |
+
|
30 |
+
end() {
|
31 |
+
return this.callback_fn();
|
32 |
+
}
|
33 |
+
}
|
34 |
+
|
35 |
+
export const Form = ({ children }: { children: React.ReactNode }) => {
|
36 |
+
const [modelLoaded, setModelLoaded] = useState(false);
|
37 |
+
const [progress, setProgress] = useState(0);
|
38 |
+
const [statusText, setStatusText] = useState("Loading model (656MB)...");
|
39 |
+
const [loadProgress, setLoadProgress] = useState({});
|
40 |
+
const [track, setTrack] = useState("");
|
41 |
+
|
42 |
+
const {
|
43 |
+
form,
|
44 |
+
setForm,
|
45 |
+
formattedPrompt,
|
46 |
+
generate,
|
47 |
+
results,
|
48 |
+
loading,
|
49 |
+
setResults,
|
50 |
+
} = useGeneration();
|
51 |
+
|
52 |
+
const modelPromise = useRef(null);
|
53 |
+
const tokenizerPromise = useRef(null);
|
54 |
+
|
55 |
+
useEffect(() => {
|
56 |
+
modelPromise.current ??= MusicgenForConditionalGeneration.from_pretrained(
|
57 |
+
MODEL_ID,
|
58 |
+
{
|
59 |
+
progress_callback: (data: any) => {
|
60 |
+
if (data.status !== "progress") return;
|
61 |
+
setLoadProgress((prev) => ({ ...prev, [data.file]: data }));
|
62 |
+
},
|
63 |
+
dtype: {
|
64 |
+
text_encoder: "q8",
|
65 |
+
decoder_model_merged: "q8",
|
66 |
+
encodec_decode: "fp32",
|
67 |
+
},
|
68 |
+
device: "wasm",
|
69 |
+
}
|
70 |
+
);
|
71 |
+
//@ts-ignore
|
72 |
+
tokenizerPromise.current ??= AutoTokenizer.from_pretrained(MODEL_ID);
|
73 |
+
}, []);
|
74 |
+
|
75 |
+
useEffect(() => {
|
76 |
+
const items = Object.values(loadProgress);
|
77 |
+
if (items.length !== 5) return; // 5 files to load
|
78 |
+
let loaded = 0;
|
79 |
+
let total = 0;
|
80 |
+
for (const data of Object.values(loadProgress)) {
|
81 |
+
loaded += data.loaded;
|
82 |
+
total += data.total;
|
83 |
+
}
|
84 |
+
const progress = loaded / total;
|
85 |
+
setProgress(progress);
|
86 |
+
setStatusText(
|
87 |
+
progress === 1
|
88 |
+
? "Ready!"
|
89 |
+
: `Loading model (${(progress * 100).toFixed()}% of 656MB)...`
|
90 |
+
);
|
91 |
+
if (progress === 1) {
|
92 |
+
setTimeout(() => setModelLoaded(true), 1500);
|
93 |
+
}
|
94 |
+
}, [loadProgress]);
|
95 |
+
|
96 |
+
const generateMusic = async () => {
|
97 |
+
const tokenizer: any = await tokenizerPromise.current;
|
98 |
+
const model: any = await modelPromise.current;
|
99 |
+
|
100 |
+
if (!tokenizer || !model) return null;
|
101 |
+
|
102 |
+
const max_length = Math.min(
|
103 |
+
Math.max(Math.floor(form.length * 50), 1) + 4,
|
104 |
+
model?.generation_config?.max_length ?? 1500
|
105 |
+
);
|
106 |
+
|
107 |
+
const streamer = new CallbackStreamer((value: string) => {
|
108 |
+
const percent = value === undefined ? 1 : value[0].length / max_length;
|
109 |
+
setStatusText(`Generating (${(percent * 100).toFixed()}%)...`);
|
110 |
+
setProgress(percent);
|
111 |
+
});
|
112 |
+
|
113 |
+
const inputs = tokenizer(formattedPrompt);
|
114 |
+
|
115 |
+
const audio_values = await model.generate({
|
116 |
+
...inputs,
|
117 |
+
max_length,
|
118 |
+
streamer,
|
119 |
+
});
|
120 |
+
|
121 |
+
setStatusText("Encoding audio...");
|
122 |
+
|
123 |
+
const sampling_rate = model.config.audio_encoder.sampling_rate;
|
124 |
+
const wav = encodeWAV(audio_values.data, sampling_rate);
|
125 |
+
const blob = new Blob([wav], { type: "audio/wav" });
|
126 |
+
setTrack(URL.createObjectURL(blob));
|
127 |
+
setStatusText("Done!");
|
128 |
+
};
|
129 |
+
|
130 |
+
console.log("track is", track);
|
131 |
+
|
132 |
+
return (
|
133 |
+
<main className="grid grid-cols-2 gap-20">
|
134 |
+
<div className="grid grid-cols-1 gap-10">
|
135 |
+
{children}
|
136 |
+
<Prompt
|
137 |
+
value={form.prompt}
|
138 |
+
onChange={(value) => setForm({ ...form, prompt: value })}
|
139 |
+
/>
|
140 |
+
<Length
|
141 |
+
value={form.length}
|
142 |
+
onChange={(value) => setForm({ ...form, length: value })}
|
143 |
+
/>
|
144 |
+
<Styles
|
145 |
+
value={form.style}
|
146 |
+
onChange={(value) => setForm({ ...form, style: value })}
|
147 |
+
/>
|
148 |
+
<Moods
|
149 |
+
value={form.mood}
|
150 |
+
onChange={(value) => setForm({ ...form, mood: value })}
|
151 |
+
/>
|
152 |
+
</div>
|
153 |
+
<div>
|
154 |
+
<div className="w-full sticky top-10">
|
155 |
+
<div className="border rounded-xl p-6 bg-stone-900/40 border-white/5">
|
156 |
+
<p className="text-amber-200 font-semibold text-xs uppercase mb-3">
|
157 |
+
Generated prompt
|
158 |
+
</p>
|
159 |
+
<p className="text-white text-xl font-semibold">
|
160 |
+
"{formattedPrompt}"
|
161 |
+
</p>
|
162 |
+
</div>
|
163 |
+
<button
|
164 |
+
className="rounded-xl bg-stone-900/90 border-white/5 border px-6 py-3 font-semibold text-base text-white mt-6 hover:brightness-125 transition-all duration-200"
|
165 |
+
onClick={() => {
|
166 |
+
generate();
|
167 |
+
generateMusic();
|
168 |
+
}}
|
169 |
+
>
|
170 |
+
Generate track
|
171 |
+
</button>
|
172 |
+
{(loading || results?.title || results?.cover) && (
|
173 |
+
<div className="mt-6 space-y-6 flex flex-col">
|
174 |
+
{results.cover ? (
|
175 |
+
<Image
|
176 |
+
src={results.cover}
|
177 |
+
alt="Cover art"
|
178 |
+
className="w-[300px] h-[300px] object-contain bg-amber-950 rounded-xl mx-auto"
|
179 |
+
width={300}
|
180 |
+
height={300}
|
181 |
+
/>
|
182 |
+
) : (
|
183 |
+
<div className="w-[300px] h-[300px] bg-stone-900 animate-pulse rounded-xl mx-auto"></div>
|
184 |
+
)}
|
185 |
+
{results.title ? (
|
186 |
+
<p className="text-center text-white font-bold text-3xl">
|
187 |
+
{results.title}
|
188 |
+
</p>
|
189 |
+
) : (
|
190 |
+
<div className="w-[450px] h-8 bg-stone-900 animate-pulse rounded-xl mx-auto"></div>
|
191 |
+
)}
|
192 |
+
{modelLoaded &&
|
193 |
+
(track !== "" ? (
|
194 |
+
<div className="mx-auto">
|
195 |
+
<audio controls type="audio/wav" src={track} />
|
196 |
+
</div>
|
197 |
+
) : (
|
198 |
+
<div className="mx-auto w-full max-w-sm border rounded-xl p-6 bg-amber-900/10 border-white/10 overflow-hidden transition-all duration-200">
|
199 |
+
<p className="text-sm text-left mb-4 uppercase font-medium">
|
200 |
+
{statusText}
|
201 |
+
</p>
|
202 |
+
<div className="bg-gray-200 h-2.5 w-full rounded-full overflow-hidden">
|
203 |
+
<div
|
204 |
+
className="from-amber-500 to-orange-500 bg-gradient-to-r h-full"
|
205 |
+
style={{ width: `${100 * progress}%` }}
|
206 |
+
></div>
|
207 |
+
</div>
|
208 |
+
</div>
|
209 |
+
))}
|
210 |
+
</div>
|
211 |
+
)}
|
212 |
+
</div>
|
213 |
+
</div>
|
214 |
+
<div
|
215 |
+
className={classNames(
|
216 |
+
"absolute right-10 bottom-10 w-full max-w-sm border rounded-xl p-6 bg-amber-900/10 border-white/10 overflow-hidden transition-all duration-200",
|
217 |
+
{
|
218 |
+
"opacity-0 pointer-events-none translate-y-full": modelLoaded,
|
219 |
+
}
|
220 |
+
)}
|
221 |
+
>
|
222 |
+
<p className="text-sm text-left mb-4 uppercase font-medium">
|
223 |
+
{statusText}
|
224 |
+
</p>
|
225 |
+
<div className="bg-gray-200 h-2.5 w-full rounded-full overflow-hidden">
|
226 |
+
<div
|
227 |
+
className="from-amber-500 to-orange-500 bg-gradient-to-r h-full"
|
228 |
+
style={{ width: `${100 * progress}%` }}
|
229 |
+
></div>
|
230 |
+
</div>
|
231 |
+
</div>
|
232 |
+
</main>
|
233 |
+
);
|
234 |
+
};
|
components/hooks/useGeneration.ts
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useMemo, useState } from "react"
|
2 |
+
import { useUpdateEffect } from "react-use";
|
3 |
+
|
4 |
+
import { LENGTHS, STYLES, MOODS } from "@/utils";
|
5 |
+
|
6 |
+
export const useGeneration = () => {
|
7 |
+
const [form, setForm] = useState({
|
8 |
+
prompt: "80s pop track with bassy drums and synth",
|
9 |
+
length: LENGTHS[0].value,
|
10 |
+
style: STYLES[5].value,
|
11 |
+
mood: MOODS[4].value
|
12 |
+
});
|
13 |
+
const [loading, setLoading] = useState(false);
|
14 |
+
const [results, setResults] = useState<{ cover: string | null, title: string | null }>({
|
15 |
+
cover: null,
|
16 |
+
title: null,
|
17 |
+
});
|
18 |
+
const [timeCoverGenerated, setTimeCoverGenerated] = useState(0);
|
19 |
+
|
20 |
+
const formattedPrompt = useMemo(() => {
|
21 |
+
const stylePrompt = STYLES.find((style) => style.value === form.style)?.prompt;
|
22 |
+
const moodPrompt = MOODS.find((mood) => mood.value === form.mood)?.prompt;
|
23 |
+
|
24 |
+
return `${form.prompt} ${stylePrompt} ${moodPrompt ?? ""}`;
|
25 |
+
}, [form])
|
26 |
+
|
27 |
+
useUpdateEffect(() => {
|
28 |
+
if (results.cover && results.title) {
|
29 |
+
setLoading(false);
|
30 |
+
}
|
31 |
+
}, [results])
|
32 |
+
|
33 |
+
const generate = async () => {
|
34 |
+
setLoading(true);
|
35 |
+
let new_results = {
|
36 |
+
cover: null,
|
37 |
+
title: null,
|
38 |
+
track: null
|
39 |
+
}
|
40 |
+
|
41 |
+
fetch("/api/generate/cover", {
|
42 |
+
method: "POST",
|
43 |
+
headers: {
|
44 |
+
"Content-Type": "application/json"
|
45 |
+
},
|
46 |
+
body: JSON.stringify({
|
47 |
+
prompt: formattedPrompt
|
48 |
+
})
|
49 |
+
}).then((res) => res.json())
|
50 |
+
.then((res: any) => {
|
51 |
+
new_results.cover = res.image
|
52 |
+
setResults({
|
53 |
+
...new_results,
|
54 |
+
})
|
55 |
+
})
|
56 |
+
|
57 |
+
fetch("/api/generate/title", {
|
58 |
+
method: "POST",
|
59 |
+
headers: {
|
60 |
+
"Content-Type": "application/json"
|
61 |
+
},
|
62 |
+
body: JSON.stringify({
|
63 |
+
prompt: form.prompt
|
64 |
+
})
|
65 |
+
}).then((res) => res.json())
|
66 |
+
.then((res: any) => {
|
67 |
+
new_results.title = res.title
|
68 |
+
setResults({
|
69 |
+
...new_results,
|
70 |
+
})
|
71 |
+
})
|
72 |
+
}
|
73 |
+
|
74 |
+
return {
|
75 |
+
form,
|
76 |
+
setForm,
|
77 |
+
formattedPrompt,
|
78 |
+
generate,
|
79 |
+
results,
|
80 |
+
loading,
|
81 |
+
setResults,
|
82 |
+
}
|
83 |
+
}
|
components/length.tsx
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import classNames from "classnames";
|
2 |
+
import { useMemo, useRef, useState } from "react";
|
3 |
+
import { useClickAway, useUpdateEffect } from "react-use";
|
4 |
+
import { FiChevronDown } from "react-icons/fi";
|
5 |
+
|
6 |
+
import { LENGTHS } from "@/utils";
|
7 |
+
|
8 |
+
export const Length = ({
|
9 |
+
value,
|
10 |
+
onChange,
|
11 |
+
}: {
|
12 |
+
value: number;
|
13 |
+
onChange: (value: number) => void;
|
14 |
+
}) => {
|
15 |
+
const [open, setOpen] = useState(false);
|
16 |
+
const ref = useRef<HTMLDivElement>(null);
|
17 |
+
|
18 |
+
useClickAway(ref, () => {
|
19 |
+
setOpen(false);
|
20 |
+
});
|
21 |
+
|
22 |
+
const label = useMemo(() => {
|
23 |
+
return LENGTHS.find((length) => length.value === value)?.label;
|
24 |
+
}, [value]);
|
25 |
+
|
26 |
+
useUpdateEffect(() => {
|
27 |
+
setOpen(false);
|
28 |
+
}, [value]);
|
29 |
+
|
30 |
+
return (
|
31 |
+
<div ref={ref} className="max-w-max">
|
32 |
+
<p className="text-white font-semibold text-base mb-4">Prompt</p>
|
33 |
+
<div className="relative z-1">
|
34 |
+
<p
|
35 |
+
className="text-transparent bg-gradient-to-r from-blue-500 to-pink-500 bg-clip-text text-5xl font-extrabold cursor-pointer relative"
|
36 |
+
onClick={() => setOpen(!open)}
|
37 |
+
>
|
38 |
+
{label}s
|
39 |
+
<FiChevronDown
|
40 |
+
className={classNames(
|
41 |
+
"inline-block text-white text-2xl ml-2 transition-all duration-200",
|
42 |
+
{
|
43 |
+
"transform rotate-180": open,
|
44 |
+
}
|
45 |
+
)}
|
46 |
+
/>
|
47 |
+
</p>
|
48 |
+
<ul
|
49 |
+
className={classNames(
|
50 |
+
"border-white/10 bg-black shadow-lg absolute top-14 p-3 left-0 w-full border border-gray-800 rounded-lg max-w-max z-20",
|
51 |
+
{
|
52 |
+
"opacity-0 pointer-events-none": !open,
|
53 |
+
}
|
54 |
+
)}
|
55 |
+
>
|
56 |
+
{LENGTHS.map((length) => (
|
57 |
+
<li
|
58 |
+
key={length.value}
|
59 |
+
className="text-white text-base hover:from-blue-500/40 hover:to-pink-500/40 bg-gradient-to-r transition-all duration-200 p-2 rounded-md cursor-pointer"
|
60 |
+
onClick={() => onChange(length.value)}
|
61 |
+
>
|
62 |
+
{length.label} seconds
|
63 |
+
</li>
|
64 |
+
))}
|
65 |
+
</ul>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
);
|
69 |
+
};
|
components/moods.tsx
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import classNames from "classnames";
|
2 |
+
import { useState } from "react";
|
3 |
+
import { FiChevronDown } from "react-icons/fi";
|
4 |
+
|
5 |
+
import { MOODS } from "@/utils";
|
6 |
+
|
7 |
+
export const Moods = ({
|
8 |
+
value,
|
9 |
+
onChange,
|
10 |
+
}: {
|
11 |
+
value: string;
|
12 |
+
onChange: (value: string) => void;
|
13 |
+
}) => {
|
14 |
+
const [viewAll, setViewAll] = useState(false);
|
15 |
+
|
16 |
+
return (
|
17 |
+
<div>
|
18 |
+
<p className="text-white font-semibold text-base mb-4">Select a mood</p>
|
19 |
+
<div className="grid grid-cols-4 gap-2 relative z-[1]">
|
20 |
+
{MOODS.slice(viewAll ? 0 : 0, viewAll ? MOODS.length : 12).map(
|
21 |
+
(style) => {
|
22 |
+
return (
|
23 |
+
<div
|
24 |
+
key={style.label}
|
25 |
+
className={classNames(
|
26 |
+
`w-full cursor-pointer transition-all duration-200 opacity-70 hover:opacity-100 rounded-xl bg-gradient-to-br bg-stone-900 border border-white/5 relative px-6 py-3.5 text-left flex items-center justify-between font-semibold text-white text-lg z-[1] overflow-hidden`,
|
27 |
+
{
|
28 |
+
"!opacity-100 brightness-125": style.value === value,
|
29 |
+
}
|
30 |
+
)}
|
31 |
+
onClick={() => onChange(style.value)}
|
32 |
+
>
|
33 |
+
{style.label}
|
34 |
+
<p className="text-3xl">{style.emoji}</p>
|
35 |
+
<div className="bg-black/30 -z-[1] w-full h-full absolute top-0 left-0"></div>
|
36 |
+
</div>
|
37 |
+
);
|
38 |
+
}
|
39 |
+
)}
|
40 |
+
<div
|
41 |
+
className={classNames(
|
42 |
+
"w-full h-[100px] bg-gradient-to-b from-transparent absolute -bottom-8 left-0 -z-[1] flex items-end justify-center",
|
43 |
+
{
|
44 |
+
"to-stone-950 !z-[1]": !viewAll,
|
45 |
+
}
|
46 |
+
)}
|
47 |
+
>
|
48 |
+
<p
|
49 |
+
className="text-white/50 hover:text-white/90 text-sm font-medium mt-2 cursor-pointer hover:underline"
|
50 |
+
onClick={() => setViewAll(!viewAll)}
|
51 |
+
>
|
52 |
+
View all
|
53 |
+
<FiChevronDown
|
54 |
+
className={classNames(
|
55 |
+
"inline-block ml-1 transition-all duration-200",
|
56 |
+
{
|
57 |
+
"transform rotate-180": viewAll,
|
58 |
+
}
|
59 |
+
)}
|
60 |
+
/>
|
61 |
+
</p>
|
62 |
+
</div>
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
);
|
66 |
+
};
|
components/prompt.tsx
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const Prompt = ({
|
2 |
+
value,
|
3 |
+
onChange,
|
4 |
+
}: {
|
5 |
+
value: string;
|
6 |
+
onChange: (value: string) => void;
|
7 |
+
}) => {
|
8 |
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
9 |
+
onChange(e.target.value);
|
10 |
+
};
|
11 |
+
return (
|
12 |
+
<div>
|
13 |
+
<p className="text-white font-semibold text-base mb-3">Prompt</p>
|
14 |
+
<input
|
15 |
+
type="text"
|
16 |
+
value={value}
|
17 |
+
placeholder="80s pop track with bassy drums and synth"
|
18 |
+
className="w-full p-2 mt-2 border border-white/10 bg-black/10 transition-all duration-200 focus:border-amber-200/20 focus:bg-amber-950/10 text-white rounded-xl px-5 py-5 text-lg outline-none"
|
19 |
+
onInput={handleChange}
|
20 |
+
/>
|
21 |
+
</div>
|
22 |
+
);
|
23 |
+
};
|
components/styles.tsx
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import classNames from "classnames";
|
2 |
+
import { useState } from "react";
|
3 |
+
import { FiChevronDown } from "react-icons/fi";
|
4 |
+
|
5 |
+
import { STYLES } from "@/utils";
|
6 |
+
|
7 |
+
export const Styles = ({
|
8 |
+
value,
|
9 |
+
onChange,
|
10 |
+
}: {
|
11 |
+
value: string;
|
12 |
+
onChange: (value: string) => void;
|
13 |
+
}) => {
|
14 |
+
const [viewAll, setViewAll] = useState(false);
|
15 |
+
|
16 |
+
return (
|
17 |
+
<div>
|
18 |
+
<p className="text-white font-semibold text-base mb-4">Select a style</p>
|
19 |
+
<div className="grid grid-cols-3 gap-2 relative z-[1]">
|
20 |
+
{STYLES.slice(viewAll ? 0 : 0, viewAll ? STYLES.length : 9).map(
|
21 |
+
(style) => (
|
22 |
+
<div
|
23 |
+
key={style.label}
|
24 |
+
className={classNames(
|
25 |
+
"w-full cursor-pointer transition-all duration-200 opacity-40 hover:opacity-100 rounded-xl bg-cover bg-center relative px-5 py-8 bg-gray-700 text-center font-bold text-white text-xl z-[1] overflow-hidden",
|
26 |
+
{
|
27 |
+
"!opacity-100 ring-[4px] ring-white/50":
|
28 |
+
style.value === value,
|
29 |
+
}
|
30 |
+
)}
|
31 |
+
style={{
|
32 |
+
backgroundImage: `url(${style.image})`,
|
33 |
+
}}
|
34 |
+
onClick={() => onChange(style.value)}
|
35 |
+
>
|
36 |
+
{style.label}
|
37 |
+
<div className="bg-black/50 -z-[1] w-full h-full absolute top-0 left-0"></div>
|
38 |
+
</div>
|
39 |
+
)
|
40 |
+
)}
|
41 |
+
<div
|
42 |
+
className={classNames(
|
43 |
+
"w-full h-[100px] bg-gradient-to-b from-transparent absolute -bottom-8 left-0 -z-[1] flex items-end justify-center",
|
44 |
+
{
|
45 |
+
"to-stone-950 !z-[1]": !viewAll,
|
46 |
+
}
|
47 |
+
)}
|
48 |
+
>
|
49 |
+
<p
|
50 |
+
className="text-white/50 hover:text-white/90 text-sm font-medium mt-2 cursor-pointer hover:underline"
|
51 |
+
onClick={() => setViewAll(!viewAll)}
|
52 |
+
>
|
53 |
+
View all
|
54 |
+
<FiChevronDown
|
55 |
+
className={classNames(
|
56 |
+
"inline-block ml-1 transition-all duration-200",
|
57 |
+
{
|
58 |
+
"transform rotate-180": viewAll,
|
59 |
+
}
|
60 |
+
)}
|
61 |
+
/>
|
62 |
+
</p>
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
</div>
|
66 |
+
);
|
67 |
+
};
|
next.config.mjs
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
webpack: (config) => {
|
4 |
+
// Ignore node-specific modules when bundling for the browser
|
5 |
+
// See https://webpack.js.org/configuration/resolve/#resolvealias
|
6 |
+
config.resolve.alias = {
|
7 |
+
...config.resolve.alias,
|
8 |
+
"sharp$": false,
|
9 |
+
"onnxruntime-node$": false,
|
10 |
+
}
|
11 |
+
return config;
|
12 |
+
},
|
13 |
+
};
|
14 |
+
|
15 |
+
export default nextConfig;
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "ai-music-generation",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"scripts": {
|
6 |
+
"dev": "next dev -p 3001",
|
7 |
+
"build": "next build",
|
8 |
+
"start": "next start",
|
9 |
+
"lint": "next lint"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"@xenova/transformers": "github:xenova/transformers.js#v3",
|
13 |
+
"classnames": "^2.5.1",
|
14 |
+
"next": "14.2.1",
|
15 |
+
"react": "^18",
|
16 |
+
"react-dom": "^18",
|
17 |
+
"react-icons": "^5.1.0",
|
18 |
+
"react-use": "^17.5.0"
|
19 |
+
},
|
20 |
+
"devDependencies": {
|
21 |
+
"@types/node": "^20",
|
22 |
+
"@types/react": "^18",
|
23 |
+
"@types/react-dom": "^18",
|
24 |
+
"eslint": "^8",
|
25 |
+
"eslint-config-next": "14.2.1",
|
26 |
+
"postcss": "^8",
|
27 |
+
"tailwindcss": "^3.4.1",
|
28 |
+
"typescript": "^5"
|
29 |
+
}
|
30 |
+
}
|
postcss.config.mjs
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('postcss-load-config').Config} */
|
2 |
+
const config = {
|
3 |
+
plugins: {
|
4 |
+
tailwindcss: {},
|
5 |
+
},
|
6 |
+
};
|
7 |
+
|
8 |
+
export default config;
|
public/next.svg
ADDED
|
public/vercel.svg
ADDED
|
tailwind.config.ts
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Config } from "tailwindcss";
|
2 |
+
|
3 |
+
const config: Config = {
|
4 |
+
content: [
|
5 |
+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
6 |
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
7 |
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
8 |
+
"./utils/**/*.{js,ts,jsx,tsx,mdx}"
|
9 |
+
],
|
10 |
+
theme: {
|
11 |
+
extend: {
|
12 |
+
backgroundImage: {
|
13 |
+
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
14 |
+
"gradient-conic":
|
15 |
+
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
16 |
+
},
|
17 |
+
},
|
18 |
+
},
|
19 |
+
plugins: [],
|
20 |
+
};
|
21 |
+
export default config;
|
tsconfig.json
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
4 |
+
"allowJs": true,
|
5 |
+
"skipLibCheck": true,
|
6 |
+
"strict": true,
|
7 |
+
"noEmit": true,
|
8 |
+
"esModuleInterop": true,
|
9 |
+
"module": "esnext",
|
10 |
+
"moduleResolution": "bundler",
|
11 |
+
"resolveJsonModule": true,
|
12 |
+
"isolatedModules": true,
|
13 |
+
"jsx": "preserve",
|
14 |
+
"incremental": true,
|
15 |
+
"plugins": [
|
16 |
+
{
|
17 |
+
"name": "next"
|
18 |
+
}
|
19 |
+
],
|
20 |
+
"paths": {
|
21 |
+
"@/*": ["./*"]
|
22 |
+
}
|
23 |
+
},
|
24 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "components/form.tsx"],
|
25 |
+
"exclude": ["node_modules"]
|
26 |
+
}
|
utils/index.ts
ADDED
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const LENGTHS = [
|
2 |
+
{ value: 5, label: "00:05" },
|
3 |
+
{ value: 10, label: "00:10" },
|
4 |
+
{ value: 15, label: "00:15" },
|
5 |
+
{ value: 30, label: "00:30" },
|
6 |
+
];
|
7 |
+
|
8 |
+
export const STYLES = [{
|
9 |
+
value: "hiphop",
|
10 |
+
prompt: "hip hop track with a chill vibe",
|
11 |
+
label: "Hip Hop",
|
12 |
+
image: "https://images.unsplash.com/photo-1601643157091-ce5c665179ab?q=80&w=2072&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
13 |
+
}, {
|
14 |
+
value: "classic",
|
15 |
+
prompt: "classic track with a chill vibe",
|
16 |
+
label: "Classic",
|
17 |
+
image: "https://images.unsplash.com/photo-1519139270028-ab664cf42264?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
18 |
+
}, {
|
19 |
+
value: "jazz",
|
20 |
+
prompt: "jazz track with a chill vibe",
|
21 |
+
label: "Jazz",
|
22 |
+
image: "https://images.unsplash.com/photo-1511192336575-5a79af67a629?q=80&w=2064&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
23 |
+
}, {
|
24 |
+
value: "electro",
|
25 |
+
prompt: "jazz track with a chill vibe",
|
26 |
+
label: "Electro & Dance",
|
27 |
+
image: "https://images.unsplash.com/photo-1622386010273-646e12d1c02f?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
28 |
+
}, {
|
29 |
+
value: "rock",
|
30 |
+
prompt: "jazz track with a chill vibe",
|
31 |
+
label: "Rock'N'Roll",
|
32 |
+
image: "https://plus.unsplash.com/premium_photo-1681876467464-33495108737c?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
33 |
+
}, {
|
34 |
+
value: "funk",
|
35 |
+
prompt: "jazz track with a chill vibe",
|
36 |
+
label: "Funk",
|
37 |
+
image: "https://plus.unsplash.com/premium_photo-1683129651802-1c7ba429a137?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
38 |
+
}, {
|
39 |
+
value: "dubstep",
|
40 |
+
prompt: "jazz track with a chill vibe",
|
41 |
+
label: "Dubstep",
|
42 |
+
image: "https://images.unsplash.com/photo-1578946956088-940c3b502864?q=80&w=2046&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
43 |
+
}, {
|
44 |
+
value: "afrobeats",
|
45 |
+
prompt: "jazz track with a chill vibe",
|
46 |
+
label: "Afrobeats",
|
47 |
+
image: "https://plus.unsplash.com/premium_photo-1702220976033-50f47c7a58a6?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
48 |
+
}, {
|
49 |
+
value: "orchestral",
|
50 |
+
prompt: "jazz track with a chill vibe",
|
51 |
+
label: "Orchestral",
|
52 |
+
image: "https://plus.unsplash.com/premium_photo-1682098438728-fa774b584c18?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
53 |
+
}, {
|
54 |
+
value: "pop",
|
55 |
+
prompt: "jazz track with a chill vibe",
|
56 |
+
label: "Pop",
|
57 |
+
image: "https://images.unsplash.com/photo-1520872024865-3ff2805d8bb3?q=80&w=2104&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
58 |
+
}, {
|
59 |
+
value: "reggae",
|
60 |
+
prompt: "jazz track with a chill vibe",
|
61 |
+
label: "Reggae",
|
62 |
+
image: "https://images.unsplash.com/photo-1538598450935-581f6a5fa7e0?q=80&w=2088&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
63 |
+
}, {
|
64 |
+
value: "metal",
|
65 |
+
prompt: "jazz track with a chill vibe",
|
66 |
+
label: "Metal",
|
67 |
+
image: "https://images.unsplash.com/photo-1506091403742-e3aa39518db5?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
68 |
+
}, {
|
69 |
+
value: "country",
|
70 |
+
prompt: "jazz track with a chill vibe",
|
71 |
+
label: "Country",
|
72 |
+
image: "https://images.unsplash.com/photo-1525814230241-7f78c608c54c?q=80&w=1976&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
73 |
+
}, {
|
74 |
+
value: "blues",
|
75 |
+
prompt: "jazz track with a chill vibe",
|
76 |
+
label: "Blues",
|
77 |
+
image: "https://plus.unsplash.com/premium_photo-1661333454734-9184250f7226?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
78 |
+
}, {
|
79 |
+
value: "soul",
|
80 |
+
prompt: "jazz track with a chill vibe",
|
81 |
+
label: "Soul",
|
82 |
+
image: "https://images.unsplash.com/photo-1581297848080-c84ac0438210?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
83 |
+
}, {
|
84 |
+
value: "rnb",
|
85 |
+
prompt: "jazz track with a chill vibe",
|
86 |
+
label: "R&B",
|
87 |
+
image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR3D-yMSiaEBtOOoxrKh8InCYzRqjn1UnYVHPhbDGkPrXH32k7i091MRvRTP7Nyts8dMJY&usqp=CAU"
|
88 |
+
}, {
|
89 |
+
value: "disco",
|
90 |
+
prompt: "jazz track with a chill vibe",
|
91 |
+
label: "Disco",
|
92 |
+
image: "https://images.unsplash.com/photo-1559424452-eeb3a13ffe2b?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
93 |
+
}, {
|
94 |
+
value: "trap",
|
95 |
+
prompt: "jazz track with a chill vibe",
|
96 |
+
label: "Trap",
|
97 |
+
image: "https://images.unsplash.com/photo-1620281428428-bce2bf9ceee4?q=80&w=1970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
98 |
+
}, {
|
99 |
+
value: "ambient",
|
100 |
+
prompt: "jazz track with a chill vibe",
|
101 |
+
label: "Ambient",
|
102 |
+
image: "https://images.unsplash.com/photo-1616085290694-4b9cc5c97a12?q=80&w=2128&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
103 |
+
}, {
|
104 |
+
value: "lofi",
|
105 |
+
prompt: "jazz track with a chill vibe",
|
106 |
+
label: "Lofi",
|
107 |
+
image: "https://miro.medium.com/v2/resize:fit:1358/0*FjF2hZ8cJQN9aBxk.jpg"
|
108 |
+
}, {
|
109 |
+
value: "chill",
|
110 |
+
prompt: "jazz track with a chill vibe",
|
111 |
+
label: "Chill",
|
112 |
+
image: "https://images.unsplash.com/photo-1531574373289-ad0d66e39ba9?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
113 |
+
}]
|
114 |
+
|
115 |
+
export const MOODS = [{
|
116 |
+
value: "happy",
|
117 |
+
label: "Happy",
|
118 |
+
prompt: "happy track with a chill vibe",
|
119 |
+
emoji: "😊"
|
120 |
+
}, {
|
121 |
+
value: "sad",
|
122 |
+
label: "Sad",
|
123 |
+
prompt: "sad track with a chill vibe",
|
124 |
+
emoji: "😢"
|
125 |
+
}, {
|
126 |
+
value: "angry",
|
127 |
+
label: "Angry",
|
128 |
+
prompt: "angry track with a chill vibe",
|
129 |
+
emoji: "😡"
|
130 |
+
}, {
|
131 |
+
value: "chill",
|
132 |
+
label: "Chill",
|
133 |
+
prompt: "chill track with a chill vibe",
|
134 |
+
emoji: "😌"
|
135 |
+
}, {
|
136 |
+
value: "romantic",
|
137 |
+
label: "Romantic",
|
138 |
+
prompt: "romantic track with a chill vibe",
|
139 |
+
emoji: "😍"
|
140 |
+
}, {
|
141 |
+
value: "epic",
|
142 |
+
label: "Epic",
|
143 |
+
prompt: "epic track with a chill vibe",
|
144 |
+
emoji: "🚀"
|
145 |
+
}, {
|
146 |
+
value: "energetic",
|
147 |
+
label: "Energetic",
|
148 |
+
prompt: "energetic track with a chill vibe",
|
149 |
+
emoji: "🔥"
|
150 |
+
}, {
|
151 |
+
value: "dreamy",
|
152 |
+
label: "Dreamy",
|
153 |
+
prompt: "dreamy track with a chill vibe",
|
154 |
+
emoji: "🌌"
|
155 |
+
}, {
|
156 |
+
value: "mysterious",
|
157 |
+
label: "Mysterious",
|
158 |
+
prompt: "mysterious track with a chill vibe",
|
159 |
+
emoji: "🕵️"
|
160 |
+
}, {
|
161 |
+
value: "relaxing",
|
162 |
+
label: "Relaxing",
|
163 |
+
prompt: "relaxing track with a chill vibe",
|
164 |
+
emoji: "😴"
|
165 |
+
}, {
|
166 |
+
value: "dark",
|
167 |
+
label: "Dark",
|
168 |
+
prompt: "dark track with a chill vibe",
|
169 |
+
emoji: "🖤"
|
170 |
+
}, {
|
171 |
+
value: "upbeat",
|
172 |
+
label: "Upbeat",
|
173 |
+
prompt: "upbeat track with a chill vibe",
|
174 |
+
emoji: "🎉"
|
175 |
+
}, {
|
176 |
+
value: "motivational",
|
177 |
+
label: "Motivational",
|
178 |
+
prompt: "motivational track with a chill vibe",
|
179 |
+
emoji: "💪"
|
180 |
+
}, {
|
181 |
+
value: "inspiring",
|
182 |
+
label: "Inspiring",
|
183 |
+
prompt: "inspiring track with a chill vibe",
|
184 |
+
emoji: "🌟"
|
185 |
+
}, {
|
186 |
+
value: "nostalgic",
|
187 |
+
label: "Nostalgic",
|
188 |
+
prompt: "nostalgic track with a chill vibe",
|
189 |
+
emoji: "📼"
|
190 |
+
}, {
|
191 |
+
value: "groovy",
|
192 |
+
label: "Groovy",
|
193 |
+
prompt: "groovy track with a chill vibe",
|
194 |
+
emoji: "🕺"
|
195 |
+
}, {
|
196 |
+
value: "melancholic",
|
197 |
+
label: "Melancholic",
|
198 |
+
prompt: "melancholic track with a chill vibe",
|
199 |
+
emoji: "😔"
|
200 |
+
}, {
|
201 |
+
value: "hopeful",
|
202 |
+
label: "Hopeful",
|
203 |
+
prompt: "hopeful track with a chill vibe",
|
204 |
+
emoji: "🌈"
|
205 |
+
}]
|
206 |
+
|
207 |
+
|
208 |
+
export function encodeWAV(samples: any[], sampleRate = 16000) {
|
209 |
+
let offset = 44;
|
210 |
+
const buffer = new ArrayBuffer(offset + samples.length * 4);
|
211 |
+
const view = new DataView(buffer);
|
212 |
+
|
213 |
+
/* RIFF identifier */
|
214 |
+
writeString(view, 0, 'RIFF')
|
215 |
+
/* RIFF chunk length */
|
216 |
+
view.setUint32(4, 36 + samples.length * 4, true)
|
217 |
+
/* RIFF type */
|
218 |
+
writeString(view, 8, 'WAVE')
|
219 |
+
/* format chunk identifier */
|
220 |
+
writeString(view, 12, 'fmt ')
|
221 |
+
/* format chunk length */
|
222 |
+
view.setUint32(16, 16, true)
|
223 |
+
/* sample format (raw) */
|
224 |
+
view.setUint16(20, 3, true)
|
225 |
+
/* channel count */
|
226 |
+
view.setUint16(22, 1, true)
|
227 |
+
/* sample rate */
|
228 |
+
view.setUint32(24, sampleRate, true)
|
229 |
+
/* byte rate (sample rate * block align) */
|
230 |
+
view.setUint32(28, sampleRate * 4, true)
|
231 |
+
/* block align (channel count * bytes per sample) */
|
232 |
+
view.setUint16(32, 4, true)
|
233 |
+
/* bits per sample */
|
234 |
+
view.setUint16(34, 32, true)
|
235 |
+
/* data chunk identifier */
|
236 |
+
writeString(view, 36, 'data')
|
237 |
+
/* data chunk length */
|
238 |
+
view.setUint32(40, samples.length * 4, true)
|
239 |
+
|
240 |
+
for (let i = 0; i < samples.length; ++i, offset += 4) {
|
241 |
+
view.setFloat32(offset, samples[i], true)
|
242 |
+
}
|
243 |
+
|
244 |
+
return buffer
|
245 |
+
}
|
246 |
+
function writeString(view: any, offset: number, string: string) {
|
247 |
+
for (let i = 0; i < string.length; ++i) {
|
248 |
+
view.setUint8(offset + i, string.charCodeAt(i))
|
249 |
+
}
|
250 |
+
}
|
251 |
+
|
252 |
+
export const MODEL_ID = 'Xenova/musicgen-small';
|