Skip to content

Commit 28f2653

Browse files
authored
Merge pull request #54 from GlebkaF/add-songsterr-source
Add songsterr source
2 parents a677de0 + 2a67eac commit 28f2653

File tree

7 files changed

+850
-45
lines changed

7 files changed

+850
-45
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { NextResponse } from "next/server";
2+
import { createGenerator } from "../../../lib/ai-generator/create-generator";
3+
import {
4+
extractSongsterrId,
5+
fetchSongsterrData,
6+
buildPromptWithMetadata,
7+
} from "../../../lib/utils/songsterr";
8+
9+
// Исключаем этот API роут из статической генерации
10+
export function generateStaticParams() {
11+
return [{ id: "this-is-a-dummy-id-for-static-build" }];
12+
}
13+
14+
interface GenerateFromSongsterrRequest {
15+
songsterrUrl: string;
16+
trackType?: string; // "Rhythm" | "Solo" | "Lead"
17+
}
18+
19+
interface GenerateFromSongsterrResponse {
20+
generationId: string;
21+
message: string;
22+
prompt: string;
23+
songData: {
24+
artist: string;
25+
title: string;
26+
};
27+
}
28+
29+
interface ErrorResponse {
30+
error: string;
31+
}
32+
33+
/**
34+
* API endpoint для генерации Chain на основе ссылки Songsterr
35+
* POST /api/generate-from-songsterr
36+
* Body: { songsterrUrl: string, trackType?: string }
37+
*/
38+
export async function POST(
39+
request: Request,
40+
): Promise<NextResponse<GenerateFromSongsterrResponse | ErrorResponse>> {
41+
try {
42+
// Получаем данные из тела запроса
43+
const { songsterrUrl, trackType } =
44+
(await request.json()) as GenerateFromSongsterrRequest;
45+
46+
// Валидация входных данных
47+
if (!songsterrUrl || typeof songsterrUrl !== "string") {
48+
return NextResponse.json(
49+
{ error: "songsterrUrl is required" },
50+
{ status: 400 },
51+
);
52+
}
53+
54+
// Шаг 1: Извлекаем ID из URL
55+
const extracted = extractSongsterrId(songsterrUrl);
56+
if (!extracted) {
57+
return NextResponse.json(
58+
{
59+
error:
60+
"Invalid Songsterr URL format. Expected format: https://www.songsterr.com/a/wsa/...-s{songId} or -s{songId}t{trackId}",
61+
},
62+
{ status: 400 },
63+
);
64+
}
65+
66+
const { songId, trackId } = extracted;
67+
console.log(
68+
`📝 Extracted Songsterr ID: ${songId}${trackId !== null ? `, Track: ${String(trackId)}` : ""}`,
69+
);
70+
71+
// Шаг 2: Получаем данные о песне из Songsterr API
72+
let songData;
73+
try {
74+
songData = await fetchSongsterrData(songId);
75+
console.log(
76+
`🎵 Fetched song data: ${songData.artist} - ${songData.title}`,
77+
);
78+
} catch (error) {
79+
return NextResponse.json(
80+
{
81+
error: `Failed to fetch song data from Songsterr: ${error instanceof Error ? error.message : "Unknown error"}`,
82+
},
83+
{ status: 500 },
84+
);
85+
}
86+
87+
// Шаг 3: Формируем промпт с метаданными с учетом конкретного trackId из URL
88+
const promptResult = buildPromptWithMetadata(songData, trackType, trackId);
89+
console.log(`💡 Generated prompt: "${promptResult.prompt}"`);
90+
console.log("📊 Metadata:", promptResult.metadata);
91+
92+
// Шаг 4: Запускаем генератор с сформированным промптом
93+
const generator = await createGenerator();
94+
const generationId: string = await generator.generate(
95+
promptResult.prompt,
96+
songsterrUrl,
97+
promptResult.metadata,
98+
);
99+
100+
console.log(`✅ Generation created with ID: ${generationId}`);
101+
102+
// Формируем ответ
103+
const response: GenerateFromSongsterrResponse = {
104+
generationId,
105+
message: "Generation created successfully from Songsterr URL",
106+
prompt: promptResult.prompt,
107+
songData: {
108+
artist: songData.artist,
109+
title: songData.title,
110+
},
111+
};
112+
113+
// Отправляем ответ
114+
return NextResponse.json(response);
115+
} catch (error) {
116+
console.error("Error in generate-from-songsterr API:", error);
117+
return NextResponse.json(
118+
{
119+
error: error instanceof Error ? error.message : "Internal server error",
120+
},
121+
{ status: 500 },
122+
);
123+
}
124+
}

components/GeneratorForm.tsx

Lines changed: 177 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,111 @@
33
import React, { useState } from "react";
44
import { useRouter } from "next/navigation";
55

6+
type InputMode = "text" | "songsterr";
7+
68
export function GeneratorForm(): React.ReactElement {
79
const router = useRouter();
10+
const [mode, setMode] = useState<InputMode>("text");
811
const [prompt, setPrompt] = useState<string>(
9-
"Metallice Enter Sandman Rhythm Guitar Main Riff"
12+
"Metallice Enter Sandman Rhythm Guitar Main Riff",
13+
);
14+
const [songsterrUrl, setSongsterrUrl] = useState<string>(
15+
"https://www.songsterr.com/a/wsa/amatory-tab-s25195",
1016
);
17+
const [trackType, setTrackType] = useState<string>("auto");
1118
const [isLoading, setIsLoading] = useState<boolean>(false);
1219
const [error, setError] = useState<string>("");
1320

1421
const handleGenerate = async (): Promise<void> => {
15-
if (!prompt.trim()) {
16-
setError("Пожалуйста, введите описание желаемого звука");
17-
return;
18-
}
22+
if (mode === "text") {
23+
// Генерация из текстового промпта
24+
if (!prompt.trim()) {
25+
setError("Пожалуйста, введите описание желаемого звука");
26+
return;
27+
}
1928

20-
setIsLoading(true);
21-
setError("");
29+
setIsLoading(true);
30+
setError("");
2231

23-
try {
24-
const response = await fetch("/api/generate-chain", {
25-
method: "POST",
26-
headers: {
27-
"Content-Type": "application/json",
28-
},
29-
body: JSON.stringify({ prompt }),
30-
});
32+
try {
33+
const response = await fetch("/api/generate-chain", {
34+
method: "POST",
35+
headers: {
36+
"Content-Type": "application/json",
37+
},
38+
body: JSON.stringify({ prompt }),
39+
});
3140

32-
if (!response.ok) {
33-
throw new Error(`Ошибка: ${response.statusText}`);
41+
if (!response.ok) {
42+
throw new Error(`Ошибка: ${response.statusText}`);
43+
}
44+
45+
const data = (await response.json()) as {
46+
generationId: string;
47+
message: string;
48+
};
49+
50+
// Перенаправляем на страницу с результатом генерации
51+
router.push(`/admin/generation/${data.generationId}`);
52+
} catch (err) {
53+
setError(err instanceof Error ? err.message : "Произошла ошибка");
54+
setIsLoading(false);
3455
}
56+
} else {
57+
// Генерация из Songsterr URL
58+
if (!songsterrUrl.trim()) {
59+
setError("Пожалуйста, введите ссылку на Songsterr");
60+
return;
61+
}
62+
63+
setIsLoading(true);
64+
setError("");
3565

36-
const data = (await response.json()) as {
37-
generationId: string;
38-
message: string;
39-
};
66+
try {
67+
const response = await fetch("/api/generate-from-songsterr", {
68+
method: "POST",
69+
headers: {
70+
"Content-Type": "application/json",
71+
},
72+
body: JSON.stringify({
73+
songsterrUrl,
74+
trackType: trackType === "auto" ? undefined : trackType,
75+
}),
76+
});
4077

41-
// Перенаправляем на страницу с результатом генерации
42-
router.push(`/admin/generation/${data.generationId}`);
43-
} catch (err) {
44-
setError(err instanceof Error ? err.message : "Произошла ошибка");
45-
setIsLoading(false);
78+
if (!response.ok) {
79+
const errorData = (await response.json()) as { error: string };
80+
throw new Error(errorData.error || `Ошибка: ${response.statusText}`);
81+
}
82+
83+
const data = (await response.json()) as {
84+
generationId: string;
85+
message: string;
86+
prompt: string;
87+
};
88+
89+
console.log(`Generated prompt from Songsterr: ${data.prompt}`);
90+
91+
// Перенаправляем на страницу с результатом генерации
92+
router.push(`/admin/generation/${data.generationId}`);
93+
} catch (err) {
94+
setError(err instanceof Error ? err.message : "Произошла ошибка");
95+
setIsLoading(false);
96+
}
4697
}
4798
};
4899

49100
const handleKeyPress = (
50-
e: React.KeyboardEvent<HTMLTextAreaElement>
101+
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
51102
): void => {
52103
if (e.key === "Enter" && !e.shiftKey) {
53104
e.preventDefault();
54105
void handleGenerate();
55106
}
56107
};
57108

109+
const isDisabled = mode === "text" ? !prompt.trim() : !songsterrUrl.trim();
110+
58111
return (
59112
<>
60113
{/* Input Section */}
@@ -64,29 +117,112 @@ export function GeneratorForm(): React.ReactElement {
64117
✨ Опишите желаемый звук
65118
</h2>
66119
</div>
120+
121+
{/* Tabs */}
122+
<div className="border-b border-gray-200">
123+
<div className="flex">
124+
<button
125+
onClick={() => {
126+
setMode("text");
127+
}}
128+
className={`px-6 py-3 text-sm font-medium transition-colors ${
129+
mode === "text"
130+
? "border-b-2 border-purple-600 text-purple-600"
131+
: "text-gray-500 hover:text-gray-700"
132+
}`}
133+
disabled={isLoading}
134+
>
135+
Текстовое описание
136+
</button>
137+
<button
138+
onClick={() => {
139+
setMode("songsterr");
140+
}}
141+
className={`px-6 py-3 text-sm font-medium transition-colors ${
142+
mode === "songsterr"
143+
? "border-b-2 border-purple-600 text-purple-600"
144+
: "text-gray-500 hover:text-gray-700"
145+
}`}
146+
disabled={isLoading}
147+
>
148+
Ссылка Songsterr
149+
</button>
150+
</div>
151+
</div>
152+
67153
<div className="p-6">
68-
<textarea
69-
value={prompt}
70-
onChange={(e) => {
71-
setPrompt(e.target.value);
72-
}}
73-
onKeyUp={handleKeyPress}
74-
placeholder="Например: Тяжелый металлический звук с дисторшном и ревербом для соло..."
75-
className="w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
76-
rows={4}
77-
disabled={isLoading}
78-
/>
154+
{mode === "text" ? (
155+
<>
156+
<textarea
157+
value={prompt}
158+
onChange={(e) => {
159+
setPrompt(e.target.value);
160+
}}
161+
onKeyUp={handleKeyPress}
162+
placeholder="Например: Metallica Enter Sandman Rhythm Guitar Main Riff"
163+
className="w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
164+
rows={4}
165+
disabled={isLoading}
166+
/>
167+
</>
168+
) : (
169+
<>
170+
<div className="space-y-4">
171+
<div>
172+
<label className="block text-sm font-medium text-gray-700 mb-2">
173+
Ссылка на Songsterr
174+
</label>
175+
<input
176+
type="text"
177+
value={songsterrUrl}
178+
onChange={(e) => {
179+
setSongsterrUrl(e.target.value);
180+
}}
181+
onKeyUp={handleKeyPress}
182+
placeholder="https://www.songsterr.com/a/wsa/..."
183+
className="w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
184+
disabled={isLoading}
185+
/>
186+
</div>
187+
<div>
188+
<label className="block text-sm font-medium text-gray-700 mb-2">
189+
Тип трека
190+
<span className="ml-2 text-xs text-gray-500">
191+
(автоопределение по популярности)
192+
</span>
193+
</label>
194+
<select
195+
value={trackType}
196+
onChange={(e) => {
197+
setTrackType(e.target.value);
198+
}}
199+
className="w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
200+
disabled={isLoading}
201+
>
202+
<option value="auto">
203+
🎯 Автоматически (рекомендуется)
204+
</option>
205+
<option value="Rhythm">Rhythm Guitar</option>
206+
<option value="Lead">Lead Guitar</option>
207+
<option value="Solo">Solo</option>
208+
</select>
209+
</div>
210+
</div>
211+
</>
212+
)}
79213
<div className="mt-4 flex items-center justify-between">
80214
<p className="text-sm text-gray-500">
81-
Нажмите Enter для генерации или используйте кнопку
215+
{mode === "text"
216+
? "Нажмите Enter для генерации или используйте кнопку"
217+
: "Введите ссылку на табы с Songsterr"}
82218
</p>
83219
<button
84220
onClick={() => {
85221
void handleGenerate();
86222
}}
87-
disabled={isLoading || !prompt.trim()}
223+
disabled={isLoading || isDisabled}
88224
className={`px-6 py-3 rounded-lg font-medium transition-all ${
89-
isLoading || !prompt.trim()
225+
isLoading || isDisabled
90226
? "bg-gray-300 text-gray-500 cursor-not-allowed"
91227
: "bg-purple-600 text-white hover:bg-purple-700 shadow-lg hover:shadow-xl"
92228
}`}

0 commit comments

Comments
 (0)