feat: analysis providers, settings UI, song search, WAV duration fix

- 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>
This commit is contained in:
AJ Avezzano
2026-04-03 18:46:17 -04:00
parent 51f67f0aeb
commit 8b9d72bc9d
22 changed files with 1803 additions and 293 deletions

View File

@@ -0,0 +1,78 @@
import 'server-only';
import type { AnalysisProvider, ProviderInfo } from "@/lib/analysis/providers";
import { anthropicProvider } from "./anthropic";
import { openaiProvider } from "./openai";
import { ollamaProvider, getOllamaModels } from "./ollama";
import { algorithmicProvider } from "./algorithmic";
import { RECOMMENDED_OLLAMA_MODELS } from "@/lib/analysis/constants";
export { getOllamaModels, RECOMMENDED_OLLAMA_MODELS };
// Registration order determines the default when the user has no saved preference.
const ALL_PROVIDERS: AnalysisProvider[] = [
anthropicProvider,
openaiProvider,
ollamaProvider,
algorithmicProvider,
];
/**
* Returns every provider with its current availability status.
* Runs all isAvailable() checks in parallel.
*/
export async function getProviderInfoList(): Promise<ProviderInfo[]> {
const results = await Promise.all(
ALL_PROVIDERS.map(async (p) => {
const availability = await p.isAvailable();
const info: ProviderInfo = {
id: p.id,
label: p.label,
type: p.type,
available: availability.available,
};
if (!availability.available && availability.reason) {
info.unavailableReason = availability.reason;
}
if (p.id === "ollama") {
info.ollamaBaseUrl = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
}
return info;
})
);
return results;
}
/**
* Returns only providers where available === true.
* The algorithmic provider is always included.
*/
export async function getAvailableProviders(): Promise<AnalysisProvider[]> {
const checks = await Promise.all(
ALL_PROVIDERS.map(async (p) => {
const availability = await p.isAvailable();
return { provider: p, available: availability.available };
})
);
return checks.filter((c) => c.available).map((c) => c.provider);
}
/**
* Looks up a provider by id. Throws with a descriptive message if not found
* or if isAvailable() returns false.
*/
export async function getProvider(id: string): Promise<AnalysisProvider> {
const provider = ALL_PROVIDERS.find((p) => p.id === id);
if (!provider) {
throw new Error(
`Unknown provider '${id}'. Available providers: ${ALL_PROVIDERS.map((p) => p.id).join(", ")}`
);
}
const availability = await provider.isAvailable();
if (!availability.available) {
throw new Error(
`Provider '${id}' is not available: ${availability.reason ?? "unknown reason"}`
);
}
return provider;
}