feat: MusicBrainz BPM enrichment + improved AI prompts
- lookupRecordingWithTags, extractBpmFromTags, extractTimeSigFromTags, getMusicBrainzRecording added to MB client - upsertSong preserves existing BPM via COALESCE on conflict - updateSongBpm helper for async enrichment writes - AnalysisInput gains confirmedBpm / confirmedTimeSigNum fields - POST /api/analyze fetches confirmed BPM from DB then MB tags before generation - All three AI providers use confirmedBpm as authoritative and build enriched userMessage - POST /api/tracks auto-registration now fetches tags via getMusicBrainzRecording - Updated User-Agent and MB client fallback URL to Gitea Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,9 @@ export const CTP_SCHEMA = {
|
||||
export const SYSTEM_PROMPT = `\
|
||||
You are an expert music producer and session musician assisting cover bands with click tracks.
|
||||
|
||||
You will receive automated BPM detection results for a song and must generate a CTP (Click Track Protocol) document describing the song's full tempo map.
|
||||
You will receive information about a song and must generate a CTP (Click Track Protocol) document describing the song's full tempo map.
|
||||
|
||||
**Use your training knowledge of the specific song.** If you recognise the title and artist, use what you know about its actual structure: section names, bar counts, time signature, and any tempo changes (ritardando, double-time, key change with tempo shift). Your training data is a valuable source — do not ignore it in favour of generic guesses.
|
||||
|
||||
CTP rules:
|
||||
- "version" must be "1.0"
|
||||
@@ -96,10 +98,15 @@ CTP rules:
|
||||
Guidelines for section layout:
|
||||
- Use typical pop/rock section names: Intro, Verse, Pre-Chorus, Chorus, Bridge, Outro
|
||||
- Estimate bar counts based on song duration and BPM (bars = duration_seconds × BPM / 60 / beats_per_bar)
|
||||
- Most songs are 4/4; note any unusual meters if you know the song
|
||||
- If you know the song has a tempo change (ritardando, double-time feel, key change with tempo shift), model it with a ramp or step section
|
||||
- Most songs are 4/4; use 3/4, 6/8, etc. if you know the song uses that meter
|
||||
- If the song has a tempo change (ritardando, double-time feel, key change with tempo shift), model it with a ramp or step section
|
||||
- If unsure about sections, use a single constant-tempo section covering the whole song
|
||||
- Use the detected BPM as the primary tempo — do not invent a different BPM unless the song is well-known to have a different tempo
|
||||
|
||||
**BPM authority:** When a "Confirmed BPM" is provided in the user message, it comes from
|
||||
MusicBrainz community tags or a reliable reference — treat it as ground truth and use it
|
||||
for all sections unless you know the song has a significant tempo change. Do not average it
|
||||
with the detected BPM or discard it. When only a "Detected BPM" is provided, use it as a
|
||||
starting point but apply your knowledge of the song if you recognise it.
|
||||
|
||||
The output is a draft for human review. Add reasonable section structure based on the song's typical arrangement.`;
|
||||
|
||||
@@ -118,9 +125,18 @@ export const anthropicProvider: AnalysisProvider = {
|
||||
},
|
||||
|
||||
async generateCTP(input: AnalysisInput): Promise<CTPDocument> {
|
||||
const { bpm, duration, title, artist, mbid, contributed_by } = input;
|
||||
const { bpm, duration, title, artist, mbid, contributed_by, confirmedBpm, confirmedTimeSigNum } = input;
|
||||
const model = process.env.ANTHROPIC_MODEL ?? "claude-opus-4-6";
|
||||
const approxBars = Math.round((duration * bpm) / 60 / 4);
|
||||
const effectiveBpm = confirmedBpm ?? bpm;
|
||||
const approxBars = Math.round((duration * effectiveBpm) / 60 / 4);
|
||||
|
||||
const bpmLine = confirmedBpm
|
||||
? `Confirmed BPM (from MusicBrainz community tags — treat as authoritative): ${confirmedBpm}\nDetected BPM (audio analysis): ${bpm}`
|
||||
: `Detected BPM (audio analysis): ${bpm}`;
|
||||
|
||||
const timeSigHint = confirmedTimeSigNum
|
||||
? `\nConfirmed time signature numerator: ${confirmedTimeSigNum}`
|
||||
: "";
|
||||
|
||||
const userMessage = `\
|
||||
Generate a CTP document for the following song:
|
||||
@@ -128,11 +144,11 @@ Generate a CTP document for the following song:
|
||||
Title: ${title ?? "Unknown Title"}
|
||||
Artist: ${artist ?? "Unknown Artist"}
|
||||
MusicBrainz ID: ${mbid ?? "unknown"}
|
||||
Detected BPM: ${bpm}
|
||||
${bpmLine}${timeSigHint}
|
||||
Duration: ${duration.toFixed(1)} seconds (~${approxBars} bars at 4/4)
|
||||
Contributed by: ${contributed_by}
|
||||
|
||||
Create a plausible section layout for this song. If this is a well-known song, use your knowledge of its actual arrangement. If not, use a sensible generic structure.`;
|
||||
If you recognise this song, use your training knowledge of its actual arrangement — section names, bar counts, time signature, and any tempo changes. If you do not recognise it, use a sensible generic structure based on the BPM and duration above.`;
|
||||
|
||||
// thinking and output_config are not yet in the SDK type definitions;
|
||||
// cast through the base param type to avoid type errors.
|
||||
|
||||
@@ -96,13 +96,22 @@ export const ollamaProvider: AnalysisProvider = {
|
||||
},
|
||||
|
||||
async generateCTP(input: AnalysisInput): Promise<CTPDocument> {
|
||||
const { ollamaModel, bpm, duration, title, artist, mbid, contributed_by } = input;
|
||||
const { ollamaModel, bpm, duration, title, artist, mbid, contributed_by, confirmedBpm, confirmedTimeSigNum } = input;
|
||||
|
||||
if (!ollamaModel) {
|
||||
throw new Error("ollamaModel is required for Ollama provider");
|
||||
}
|
||||
|
||||
const approxBars = Math.round((duration * bpm) / 60 / 4);
|
||||
const effectiveBpm = confirmedBpm ?? bpm;
|
||||
const approxBars = Math.round((duration * effectiveBpm) / 60 / 4);
|
||||
|
||||
const bpmLine = confirmedBpm
|
||||
? `Confirmed BPM (from MusicBrainz community tags — treat as authoritative): ${confirmedBpm}\nDetected BPM (audio analysis): ${bpm}`
|
||||
: `Detected BPM (audio analysis): ${bpm}`;
|
||||
|
||||
const timeSigHint = confirmedTimeSigNum
|
||||
? `\nConfirmed time signature numerator: ${confirmedTimeSigNum}`
|
||||
: "";
|
||||
|
||||
const userMessage = `\
|
||||
Generate a CTP document for the following song:
|
||||
@@ -110,11 +119,11 @@ Generate a CTP document for the following song:
|
||||
Title: ${title ?? "Unknown Title"}
|
||||
Artist: ${artist ?? "Unknown Artist"}
|
||||
MusicBrainz ID: ${mbid ?? "unknown"}
|
||||
Detected BPM: ${bpm}
|
||||
${bpmLine}${timeSigHint}
|
||||
Duration: ${duration.toFixed(1)} seconds (~${approxBars} bars at 4/4)
|
||||
Contributed by: ${contributed_by}
|
||||
|
||||
Create a plausible section layout for this song. If this is a well-known song, use your knowledge of its actual arrangement. If not, use a sensible generic structure.`;
|
||||
If you recognise this song, use your training knowledge of its actual arrangement — section names, bar counts, time signature, and any tempo changes. If you do not recognise it, use a sensible generic structure based on the BPM and duration above.`;
|
||||
|
||||
// Attempt parse with one retry on failure
|
||||
let content: string;
|
||||
|
||||
@@ -38,8 +38,17 @@ export const openaiProvider: AnalysisProvider = {
|
||||
|
||||
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
||||
const model = process.env.OPENAI_MODEL ?? "gpt-4o";
|
||||
const { bpm, duration, title, artist, mbid, contributed_by } = input;
|
||||
const approxBars = Math.round((duration * bpm) / 60 / 4);
|
||||
const { bpm, duration, title, artist, mbid, contributed_by, confirmedBpm, confirmedTimeSigNum } = input;
|
||||
const effectiveBpm = confirmedBpm ?? bpm;
|
||||
const approxBars = Math.round((duration * effectiveBpm) / 60 / 4);
|
||||
|
||||
const bpmLine = confirmedBpm
|
||||
? `Confirmed BPM (from MusicBrainz community tags — treat as authoritative): ${confirmedBpm}\nDetected BPM (audio analysis): ${bpm}`
|
||||
: `Detected BPM (audio analysis): ${bpm}`;
|
||||
|
||||
const timeSigHint = confirmedTimeSigNum
|
||||
? `\nConfirmed time signature numerator: ${confirmedTimeSigNum}`
|
||||
: "";
|
||||
|
||||
const userMessage = `\
|
||||
Generate a CTP document for the following song:
|
||||
@@ -47,11 +56,11 @@ Generate a CTP document for the following song:
|
||||
Title: ${title ?? "Unknown Title"}
|
||||
Artist: ${artist ?? "Unknown Artist"}
|
||||
MusicBrainz ID: ${mbid ?? "unknown"}
|
||||
Detected BPM: ${bpm}
|
||||
${bpmLine}${timeSigHint}
|
||||
Duration: ${duration.toFixed(1)} seconds (~${approxBars} bars at 4/4)
|
||||
Contributed by: ${contributed_by}
|
||||
|
||||
Create a plausible section layout for this song. If this is a well-known song, use your knowledge of its actual arrangement. If not, use a sensible generic structure.`;
|
||||
If you recognise this song, use your training knowledge of its actual arrangement — section names, bar counts, time signature, and any tempo changes. If you do not recognise it, use a sensible generic structure based on the BPM and duration above.`;
|
||||
|
||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user