- Multi-provider AI analysis (Anthropic, OpenAI, Ollama, Algorithmic) - server-only guards on all provider files; client bundle fix - /settings page with provider status, Ollama model picker, preferences - Song search box on /analyze replacing raw MBID input (debounced, keyboard nav) - Auto-register song via MusicBrainz on POST /api/tracks (no more 404) - Fix WAV duration bug: last section songEnd was double-counting elapsed time - Registry sync comment updated for self-hosted HTTPS git servers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
132 lines
3.6 KiB
TypeScript
132 lines
3.6 KiB
TypeScript
import type { CTPDocument } from "@/lib/ctp/schema";
|
|
import type { AnalysisInput, AnalysisProvider } from "@/lib/analysis/providers";
|
|
|
|
// Section templates keyed by duration bucket
|
|
interface SectionTemplate {
|
|
labels: string[];
|
|
weights: number[];
|
|
}
|
|
|
|
function getTemplate(duration: number): SectionTemplate {
|
|
if (duration < 120) {
|
|
return {
|
|
labels: ["Intro", "Verse / Chorus", "Outro"],
|
|
weights: [0.12, 0.76, 0.12],
|
|
};
|
|
}
|
|
if (duration < 240) {
|
|
return {
|
|
labels: ["Intro", "Verse", "Chorus", "Verse + Chorus", "Outro"],
|
|
weights: [0.08, 0.22, 0.20, 0.38, 0.12],
|
|
};
|
|
}
|
|
if (duration < 360) {
|
|
return {
|
|
labels: ["Intro", "Verse", "Chorus", "Verse + Chorus", "Bridge", "Outro"],
|
|
weights: [0.07, 0.20, 0.18, 0.33, 0.10, 0.12],
|
|
};
|
|
}
|
|
return {
|
|
labels: ["Intro", "Verse", "Chorus", "Verse + Chorus", "Instrumental", "Bridge", "Outro"],
|
|
weights: [0.06, 0.18, 0.16, 0.30, 0.10, 0.10, 0.10],
|
|
};
|
|
}
|
|
|
|
function buildFallback(input: AnalysisInput): CTPDocument {
|
|
return {
|
|
version: "1.0",
|
|
metadata: {
|
|
title: input.title ?? "Unknown Title",
|
|
artist: input.artist ?? "Unknown Artist",
|
|
mbid: input.mbid ?? null,
|
|
duration_seconds: input.duration,
|
|
contributed_by: input.contributed_by,
|
|
verified: false,
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
count_in: {
|
|
enabled: true,
|
|
bars: 2,
|
|
use_first_section_tempo: true,
|
|
},
|
|
sections: [
|
|
{
|
|
label: "Song",
|
|
start_bar: 1,
|
|
bpm: input.bpm,
|
|
time_signature: { numerator: 4, denominator: 4 },
|
|
transition: "step",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export const algorithmicProvider: AnalysisProvider = {
|
|
id: "algorithmic",
|
|
label: "Algorithmic (no AI)",
|
|
type: "algorithmic",
|
|
|
|
async isAvailable() {
|
|
return { available: true };
|
|
},
|
|
|
|
async generateCTP(input: AnalysisInput): Promise<CTPDocument> {
|
|
try {
|
|
const { bpm, duration, title } = input;
|
|
|
|
const totalBars = Math.floor((duration * bpm) / 240);
|
|
const template = getTemplate(duration);
|
|
const { labels, weights } = template;
|
|
|
|
// Allocate bars per section
|
|
const rawBars = weights.map((w) => Math.round(totalBars * w));
|
|
|
|
// Adjust last section so total is exact
|
|
const allocatedSum = rawBars.reduce((a, b) => a + b, 0);
|
|
const diff = totalBars - allocatedSum;
|
|
rawBars[rawBars.length - 1] = Math.max(1, rawBars[rawBars.length - 1] + diff);
|
|
|
|
// Determine time signature
|
|
const lowerTitle = title?.toLowerCase() ?? "";
|
|
const numerator = lowerTitle.includes("waltz") || lowerTitle.includes("3/4") ? 3 : 4;
|
|
const timeSignature = { numerator, denominator: 4 as const };
|
|
|
|
// Build sections with cumulative start_bar
|
|
let currentBar = 1;
|
|
const sections = labels.map((label, i) => {
|
|
const start_bar = currentBar;
|
|
currentBar += rawBars[i];
|
|
return {
|
|
label,
|
|
start_bar,
|
|
bpm,
|
|
time_signature: timeSignature,
|
|
transition: "step" as const,
|
|
};
|
|
});
|
|
|
|
return {
|
|
version: "1.0",
|
|
metadata: {
|
|
title: input.title ?? "Unknown Title",
|
|
artist: input.artist ?? "Unknown Artist",
|
|
mbid: input.mbid ?? null,
|
|
duration_seconds: duration,
|
|
contributed_by: input.contributed_by,
|
|
verified: false,
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
count_in: {
|
|
enabled: true,
|
|
bars: 2,
|
|
use_first_section_tempo: true,
|
|
},
|
|
sections,
|
|
};
|
|
} catch {
|
|
// Algorithmic provider must never surface an error
|
|
return buildFallback(input);
|
|
}
|
|
},
|
|
};
|