Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function OPTIONS() {
* - id (required): The unique identifier of the account (UUID)
*
* @param request - The request object
* @param params.params
* @param params - Route params containing the account ID
* @returns A NextResponse with account data
*/
Expand Down
5 changes: 5 additions & 0 deletions app/api/admins/privy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { getPrivyLoginsHandler } from "@/lib/admins/privy/getPrivyLoginsHandler"
* Returns Privy login statistics for the requested time period.
* Supports daily (last 24h), weekly (last 7 days), and monthly (last 30 days) periods.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getPrivyLoginsHandler(request);
}

/**
*
*/
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}
44 changes: 44 additions & 0 deletions app/api/artists/intel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { generateArtistIntelPackHandler } from "@/lib/artistIntel/generateArtistIntelPackHandler";

/**
* OPTIONS handler for CORS preflight requests.
*
* @returns A NextResponse with CORS headers.
*/
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: getCorsHeaders(),
});
}

/**
* POST /api/artists/intel
*
* Generates a complete Artist Intelligence Pack for any artist.
*
* Combines three data sources in parallel:
* 1. Spotify — Artist profile (name, genres, followers, popularity) + top tracks with
* 30-second audio preview URLs.
* 2. MusicFlamingo (NVIDIA 8B) — AI audio analysis of the artist's top track via the
* Spotify preview URL: genre/BPM/key/mood (catalog_metadata), target audience
* demographics (audience_profile), playlist pitch targets (playlist_pitch), and
* mood/vibe tags (mood_tags).
* 3. Perplexity — Real-time web research: recent press, streaming news, and trending
* moments for the artist.
*
* An AI marketing strategist then synthesizes all three sources into a ready-to-use
* marketing pack: playlist pitch email, Instagram/TikTok/Twitter captions, press
* release opener, and key talking points.
*
* Request body:
* - artist_name (required): The artist name to analyze (e.g. "Taylor Swift", "Bad Bunny").
*
* @param request - The request object containing a JSON body.
* @returns A NextResponse with the complete intelligence pack (200) or an error.
*/
export async function POST(request: NextRequest) {
return generateArtistIntelPackHandler(request);
}
2 changes: 2 additions & 0 deletions app/api/coding-agent/[platform]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@/lib/coding-agent/handlers/registerHandlers";
* Handles webhook verification handshakes (e.g. WhatsApp hub.challenge).
*
* @param request - The incoming verification request
* @param params.params
* @param params - Route params containing the platform name
Comment on lines +13 to 14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect JSDoc parameter: @param params.params is nonsensical.

The parameter structure is { params }: { params: Promise<{ platform: string }> }, so the correct documentation should reference params.platform, not params.params.

📝 Suggested fix
- * `@param` params.params
  * `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param params.params
* @param params - Route params containing the platform name
* `@param` params - Route params containing the platform name
* `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/coding-agent/`[platform]/route.ts around lines 13 - 14, The JSDoc
incorrectly documents `@param params.params`; update the comment to reference
the actual parameter structure and property — replace `@param params.params`
with `@param params.platform` and update the description to indicate that
`params` is an object (or promise-resolved object) containing the `platform`
string used by the route handler (the handler signature is `{ params }: {
params: Promise<{ platform: string }> }`). Ensure the JSDoc matches the
handler's parameter name and property (`params.platform`) and remove the
nonsensical `params.params` entry.

*/
export async function GET(
Expand All @@ -34,6 +35,7 @@ export async function GET(
* Handles Slack and WhatsApp webhooks via dynamic [platform] segment.
*
* @param request - The incoming webhook request
* @param params.params
* @param params - Route params containing the platform name
Comment on lines +38 to 39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same JSDoc error repeated for POST handler.

📝 Suggested fix
- * `@param` params.params
  * `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/coding-agent/`[platform]/route.ts around lines 38 - 39, The JSDoc for
both the GET and POST route handlers contains a duplicate/incorrect `@param` entry
("@param params.params" and "@param params"); update the JSDoc for the GET and
POST functions (the exported GET and POST handlers) to remove the redundant
"@param params.params" line and keep a single correct parameter doc like "@param
params - Route params containing the platform name" (or rename to "@param
param0" if matching the handler signature), ensuring the doc matches the handler
signature exactly.

*/
export async function POST(
Expand Down
1 change: 1 addition & 0 deletions app/api/songs/analyze/presets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function OPTIONS() {
* - status: "success"
* - presets: Array of { name, label, description, requiresAudio, responseFormat }
*
* @param request
* @returns A NextResponse with the list of available presets
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
Expand Down
4 changes: 4 additions & 0 deletions app/api/transcribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
import { processAudioTranscription } from "@/lib/transcribe/processAudioTranscription";
import { formatTranscriptionError } from "@/lib/transcribe/types";

/**
*
* @param req
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ vi.mock("@/lib/admins/validateAdminAuth", () => ({
validateAdminAuth: vi.fn(),
}));

/**
*
* @param url
*/
function createMockRequest(url: string): NextRequest {
return {
url,
Expand Down
3 changes: 3 additions & 0 deletions lib/admins/privy/countNewAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { getCutoffMs } from "./getCutoffMs";

/**
* Counts how many users in the list were created within the cutoff period.
*
* @param users
* @param period
*/
export function countNewAccounts(users: User[], period: PrivyLoginsPeriod): number {
const cutoffMs = getCutoffMs(period);
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/privy/fetchPrivyLogins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type FetchPrivyLoginsResult = {
totalPrivyUsers: number;
};

/**
*
* @param period
*/
export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {
const isAll = period === "all";
const cutoffMs = getCutoffMs(period);
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getCutoffMs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PERIOD_DAYS } from "./periodDays";
* Returns the cutoff timestamp in milliseconds for a given period.
* Uses midnight UTC calendar day boundaries to match Privy dashboard behavior.
* Returns 0 for "all" (no cutoff).
*
* @param period
*/
export function getCutoffMs(period: PrivyLoginsPeriod): number {
if (period === "all") return 0;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getLatestVerifiedAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { User } from "@privy-io/node";
/**
* Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user.
* Returns null if no linked account has a latest_verified_at.
*
* @param user
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use account-specific terminology in JSDoc param naming.

@param user conflicts with the repository terminology rule. Rename to account-focused wording (for example, @param accountHolder), and add a brief description for clarity.

As per coding guidelines, "Use 'account' terminology, never 'entity' or 'user'; use specific names like 'artist', 'workspace', 'organization' when referring to specific types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/admins/privy/getLatestVerifiedAt.ts` at line 8, The JSDoc currently uses
"@param user" which violates the repo's account-first terminology; update the
JSDoc for getLatestVerifiedAt to use an account-focused name (for example
"@param accountHolder") and add a short description (e.g., "the account to
retrieve verification info for"). Also consider renaming the function parameter
from "user" to "accountHolder" in the getLatestVerifiedAt signature (and update
all internal references) so the doc and code consistently use account
terminology.

*/
export function getLatestVerifiedAt(user: User): number | null {
const linkedAccounts = user.linked_accounts;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/toMs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Normalizes a Privy timestamp to milliseconds.
* Privy docs say milliseconds but examples show seconds (10 digits).
*
* @param timestamp
*/
export function toMs(timestamp: number): number {
return timestamp > 1e12 ? timestamp : timestamp * 1000;
Expand Down
1 change: 1 addition & 0 deletions lib/ai/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";

/**
* Returns a specific model by its ID from the list of available models.
*
* @param modelId - The ID of the model to find
* @returns The matching model or undefined if not found
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/ai/isEmbedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";
/**
* Determines if a model is an embedding model (not suitable for chat).
* Embed models typically have 0 output pricing since they only produce embeddings.
*
* @param m
*/
export const isEmbedModel = (m: GatewayLanguageModelEntry): boolean => {
const pricing = m.pricing;
Expand Down
134 changes: 134 additions & 0 deletions lib/artistIntel/__tests__/computeArtistOpportunityScores.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, it, expect } from "vitest";
import { computeArtistOpportunityScores } from "@/lib/artistIntel/computeArtistOpportunityScores";

describe("computeArtistOpportunityScores", () => {
describe("pre-market artist (very low followers + low popularity)", () => {
it("detects a pre-market artist like Gatsby Grace (2 followers, 0 popularity)", () => {
const scores = computeArtistOpportunityScores(
null, // no music analysis (no preview URL)
2, // followers — real Gatsby Grace Spotify data
0, // popularity — real Gatsby Grace Spotify data
null, // no peer benchmark (Spotify has no related artists for pre-market acts)
);

// All scores should be in the weak-to-moderate range — early stage, not established
expect(scores.sync.score).toBe(30); // 30 + 0*0.4 = 30
expect(scores.sync.rating).toBe("weak");
expect(scores.sync.rationale).toContain("Audio analysis unavailable");

expect(scores.playlist.score).toBe(35); // 35 + 0*0.5 = 35
expect(scores.playlist.rating).toBe("weak");

// A&R rationale should say "pre-market" — NOT "established/saturated"
expect(scores.ar.rationale).toContain("Pre-market artist");
expect(scores.ar.rationale).not.toContain("established");
expect(scores.ar.rationale).not.toContain("saturated");
expect(scores.ar.score).toBeGreaterThan(40); // gets a 10-pt early-discovery bonus

expect(scores.brand.score).toBe(35); // 35 + 0*0.35 = 35
expect(scores.brand.rating).toBe("weak");

// Overall should reflect early-stage reality
expect(scores.overall).toBeGreaterThan(0);
expect(scores.overall).toBeLessThan(50);
});

it("uses growing/maturing rationale for artist with many followers but low popularity", () => {
const scores = computeArtistOpportunityScores(
null,
5_000_000, // high followers
5, // but very low popularity — not getting algorithm play
null,
);

// Should NOT trigger pre-market — this is an established artist with low traction
expect(scores.ar.rationale).not.toContain("Pre-market artist");
expect(scores.ar.rationale).toContain("growing or maturing");
});

it("gives higher efficiency bonus when popularity is high relative to small audience", () => {
// Artist with 500 followers but 60 popularity = punching way above weight
const scores = computeArtistOpportunityScores(null, 500, 60, null);

// followerEfficiency = 60 / log10(501) ≈ 60 / 2.7 ≈ 22.2 → "High" bonus (+25)
expect(scores.ar.score).toBeGreaterThan(60);
expect(scores.ar.rationale).toContain("High popularity-to-follower ratio");
});
});

describe("with music analysis", () => {
const fullMusicAnalysis = {
catalog_metadata: {
genre: "indie pop",
subgenres: ["bedroom pop"],
tempo_bpm: 110,
key: "C major",
time_signature: "4/4",
energy_level: 6,
danceability: 7,
mood: ["uplifting", "nostalgic", "dreamy"],
instruments: ["guitar", "piano", "synth", "drums"],
vocal_style: "breathy",
production_style: "polished, studio-quality",
similar_artists: ["Clairo", "Phoebe Bridgers"],
description: "Dreamy indie pop with bedroom vibes",
},
audience_profile: {
age_range: "18-24",
gender_skew: "female",
lifestyle_tags: ["coffee shops", "college campus", "late nights", "aesthetics"],
listening_contexts: ["studying", "commuting", "late night"],
platforms: ["Spotify", "Apple Music", "TikTok"],
comparable_fanbases: ["Clairo fans", "Phoebe Bridgers fans"],
marketing_hook: "The soundtrack to your 3am thoughts",
},
playlist_pitch: "Perfect for late-night indie playlists and bedroom pop discovery",
mood_tags: {
tags: ["dreamy", "nostalgic", "cozy"],
primary_mood: "contemplative",
},
};

it("computes sync score from BPM and energy data", () => {
const scores = computeArtistOpportunityScores(fullMusicAnalysis, 10_000, 45, null);

// BPM 110 (in 70-130 range): +10
// Energy 6 (in 3-8 range): +10
// 3 moods: +8
// polished production: +7
// 4 instruments: +5
// base: 50
// total: 90 → capped at 100 → "exceptional"
expect(scores.sync.score).toBeGreaterThan(60);
expect(scores.sync.rationale).toContain("BPM");
});

it("computes playlist score from danceability", () => {
const scores = computeArtistOpportunityScores(fullMusicAnalysis, 10_000, 45, null);

// danceability 7: 7*5 = 35 points
// energy 6: +5 for moderate
// popularity 45 * 0.15 = +7
// base: 40
// total: 87 → "exceptional"
expect(scores.playlist.score).toBeGreaterThan(70);
expect(scores.playlist.rationale).toContain("Danceability");
});
});

describe("overall score weighting", () => {
it("computes overall as weighted average of four scores", () => {
const scores = computeArtistOpportunityScores(null, 100_000, 50, null);

// expected overall = Math.round(ar*0.3 + playlist*0.25 + sync*0.25 + brand*0.2)
const expectedOverall = Math.round(
scores.ar.score * 0.3 +
scores.playlist.score * 0.25 +
scores.sync.score * 0.25 +
scores.brand.score * 0.2,
);

expect(scores.overall).toBe(expectedOverall);
});
});
});
Loading
Loading