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
13 changes: 5 additions & 8 deletions src/artists/getBatchArtistSocials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,20 @@ import { getArtistSocials } from "../recoup/getArtistSocials";
/**
* Fetches socials for all artists in parallel.
* Returns a Map of artistId -> socials array.
*
* @param artistIds
*/
export async function getBatchArtistSocials(
artistIds: string[]
artistIds: string[],
): Promise<Map<string, Awaited<ReturnType<typeof getArtistSocials>>>> {
logger.log("Fetching socials for all artists", {
totalArtists: artistIds.length,
});

const socialsResponses = await Promise.all(
artistIds.map((artistId) => getArtistSocials(artistId))
);
const socialsResponses = await Promise.all(artistIds.map(artistId => getArtistSocials(artistId)));

// Store socials in map
const artistSocialsMap = new Map<
string,
Awaited<ReturnType<typeof getArtistSocials>>
>();
const artistSocialsMap = new Map<string, Awaited<ReturnType<typeof getArtistSocials>>>();

for (let i = 0; i < artistIds.length; i++) {
artistSocialsMap.set(artistIds[i], socialsResponses[i]);
Expand Down
4 changes: 3 additions & 1 deletion src/artists/isScrapableSocial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { ArtistSocialProfile } from "../recoup/getArtistSocials";
*
* Non-scrapable platforms:
* - open.spotify.com
*
* @param social
*/
export function isScrapableSocial(social: ArtistSocialProfile): boolean {
const profileUrl = social.profile_url.toLowerCase();
Expand Down Expand Up @@ -39,5 +41,5 @@ export function isScrapableSocial(social: ArtistSocialProfile): boolean {
"youtube.com",
];

return scrapableDomains.some((domain) => profileUrl.includes(domain));
return scrapableDomains.some(domain => profileUrl.includes(domain));
}
4 changes: 1 addition & 3 deletions src/chats/getTaskRoomId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ type GetTaskRoomIdParams = {
* @param params - Parameters containing optional roomId and artistId
* @returns The room ID, or undefined if creation fails
*/
export async function getTaskRoomId(
params: GetTaskRoomIdParams
): Promise<string | undefined> {
export async function getTaskRoomId(params: GetTaskRoomIdParams): Promise<string | undefined> {
if (params.roomId) {
logger.log("Using existing roomId", { roomId: params.roomId });
return params.roomId;
Expand Down
2 changes: 1 addition & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const NEW_API_BASE_URL = "https://recoup-api.vercel.app";
export const RECOUP_API_KEY = process.env.RECOUP_API_KEY;
export const CODING_AGENT_ACCOUNT_ID = "04e3aba9-c130-4fb8-8b92-34e95d43e66b";
export const OPENCLAW_DEFAULT_MODEL = "vercel-ai-gateway/anthropic/claude-sonnet-4.6";
export const OPENCLAW_DEFAULT_MODEL = "vercel-ai-gateway/anthropic/claude-sonnet-4.6";
15 changes: 9 additions & 6 deletions src/content/analyzeClips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export interface SongClip {
* @param lyrics - Timestamped lyrics from transcription
* @returns Array of clip recommendations
*/
export async function analyzeClips(
songTitle: string,
lyrics: SongLyrics,
): Promise<SongClip[]> {
export async function analyzeClips(songTitle: string, lyrics: SongLyrics): Promise<SongClip[]> {
const recoupApiKey = process.env.RECOUP_API_KEY;
if (!recoupApiKey) {
throw new Error("RECOUP_API_KEY is required for clip analysis");
Expand Down Expand Up @@ -107,7 +104,10 @@ IMPORTANT: startSeconds must align with actual word timestamps from the lyrics a
return [
{
startSeconds: 0,
lyrics: lyrics.segments.slice(0, 10).map(s => s.text).join(" "),
lyrics: lyrics.segments
.slice(0, 10)
.map(s => s.text)
.join(" "),
reason: "fallback — start of song",
mood: "unknown",
hasLyrics: true,
Expand All @@ -123,7 +123,10 @@ IMPORTANT: startSeconds must align with actual word timestamps from the lyrics a
return [
{
startSeconds: 0,
lyrics: lyrics.segments.slice(0, 10).map(s => s.text).join(" "),
lyrics: lyrics.segments
.slice(0, 10)
.map(s => s.text)
.join(" "),
reason: "fallback — JSON parse failed",
mood: "unknown",
hasLyrics: true,
Expand Down
13 changes: 12 additions & 1 deletion src/content/defaultPipelineConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
* in their repo, but it's not required.
*/

type AspectRatio = "auto" | "21:9" | "16:9" | "3:2" | "4:3" | "5:4" | "1:1" | "4:5" | "3:4" | "2:3" | "9:16";
type AspectRatio =
| "auto"
| "21:9"
| "16:9"
| "3:2"
| "4:3"
| "5:4"
| "1:1"
| "4:5"
| "3:4"
| "2:3"
| "9:16";
type Resolution = "1K" | "2K" | "4K";

export interface PipelineConfig {
Expand Down
9 changes: 5 additions & 4 deletions src/content/fetchArtistContext.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
/**
* Fetches artist context (artist.md) from GitHub.
* Returns placeholder if not found.
*
* @param githubRepo
* @param artistSlug
* @param fetchFile
*/
export async function fetchArtistContext(
githubRepo: string,
artistSlug: string,
fetchFile: (repo: string, path: string) => Promise<Buffer | null>,
): Promise<string> {
const buffer = await fetchFile(
githubRepo,
`artists/${artistSlug}/context/artist.md`,
);
const buffer = await fetchFile(githubRepo, `artists/${artistSlug}/context/artist.md`);
if (!buffer) return "(no artist identity available)";
const content = buffer.toString("utf-8");
// Strip frontmatter if present
Expand Down
14 changes: 9 additions & 5 deletions src/content/fetchAudienceContext.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
/**
* Fetches audience context (audience.md) from GitHub.
* Returns placeholder if not found.
*
* @param githubRepo
* @param artistSlug
* @param fetchFile
*/
export async function fetchAudienceContext(
githubRepo: string,
artistSlug: string,
fetchFile: (repo: string, path: string) => Promise<Buffer | null>,
): Promise<string> {
const buffer = await fetchFile(
githubRepo,
`artists/${artistSlug}/context/audience.md`,
);
const buffer = await fetchFile(githubRepo, `artists/${artistSlug}/context/audience.md`);
if (!buffer) return "(no audience context available)";
return buffer.toString("utf-8").replace(/^---[\s\S]*?---\s*/, "").trim();
return buffer
.toString("utf-8")
.replace(/^---[\s\S]*?---\s*/, "")
.trim();
}
18 changes: 14 additions & 4 deletions src/content/fetchGithubFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { logger } from "@trigger.dev/sdk/v3";
* Fetches a raw file from a GitHub repo via the API.
* If the file isn't found in the main repo, checks submodule org repos.
*
* @param githubRepoUrl
* @param filePath
* @returns The file as a Buffer, or null if not found anywhere.
*/
export async function fetchGithubFile(
Expand Down Expand Up @@ -39,6 +41,10 @@ export async function fetchGithubFile(

/**
* Fetches a file from a specific GitHub repo.
*
* @param githubRepoUrl
* @param filePath
* @param token
*/
async function fetchFileFromRepo(
githubRepoUrl: string,
Expand Down Expand Up @@ -67,11 +73,11 @@ async function fetchFileFromRepo(

/**
* Reads .gitmodules from the main repo and extracts org submodule URLs.
*
* @param githubRepoUrl
* @param token
*/
async function getOrgRepoUrls(
githubRepoUrl: string,
token: string,
): Promise<string[]> {
async function getOrgRepoUrls(githubRepoUrl: string, token: string): Promise<string[]> {
const gitmodules = await fetchFileFromRepo(githubRepoUrl, ".gitmodules", token);
if (!gitmodules) return [];

Expand All @@ -87,6 +93,10 @@ async function getOrgRepoUrls(
return urls;
}

/**
*
* @param githubRepoUrl
*/
function parseRepoUrl(githubRepoUrl: string): { owner: string; repo: string } {
const match = githubRepoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
if (!match) {
Expand Down
36 changes: 23 additions & 13 deletions src/content/generateAudioVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ const execFileAsync = promisify(execFile);
*
* Matches the content-creation-app's generateAudioVideo.ts behavior.
*
* @param imageUrl.imageUrl
* @param imageUrl - URL of the AI-generated image
* @param songBuffer - Raw mp3 bytes of the song
* @param audioStartSeconds - Where to clip the song from
* @param audioDurationSeconds - How long the clip should be
* @param motionPrompt - Describes how the subject should move
* @param imageUrl.songBuffer
* @param imageUrl.audioStartSeconds
* @param imageUrl.audioDurationSeconds
* @param imageUrl.motionPrompt
Comment on lines +19 to +28
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

Malformed JSDoc @param annotations.

The JSDoc parameter annotations are incorrectly formatted. They reference imageUrl.imageUrl, imageUrl.songBuffer, etc., but the function uses destructured parameters at the top level (imageUrl, songBuffer, audioStartSeconds, etc.). This appears to be auto-generated documentation that didn't correctly parse the parameter structure.

📝 Proposed fix for JSDoc
 * Matches the content-creation-app's generateAudioVideo.ts behavior.
 *
- * `@param` imageUrl.imageUrl
  * `@param` imageUrl - URL of the AI-generated image
  * `@param` songBuffer - Raw mp3 bytes of the song
  * `@param` audioStartSeconds - Where to clip the song from
  * `@param` audioDurationSeconds - How long the clip should be
  * `@param` motionPrompt - Describes how the subject should move
- * `@param` imageUrl.songBuffer
- * `@param` imageUrl.audioStartSeconds
- * `@param` imageUrl.audioDurationSeconds
- * `@param` imageUrl.motionPrompt
  * `@returns` URL of the generated video (with audio baked in)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/generateAudioVideo.ts` around lines 19 - 28, The JSDoc `@param`
lines are malformed (e.g., "imageUrl.imageUrl", "imageUrl.songBuffer") and must
match the actual function parameters; update the JSDoc for the function
(generateAudioVideo or the top-level exported function in
src/content/generateAudioVideo.ts) to use correct `@param` names: imageUrl,
songBuffer, audioStartSeconds, audioDurationSeconds, motionPrompt, and provide
short descriptions for each, removing any "imageUrl." prefixes so the
annotations align with the destructured parameters used in the implementation.

* @returns URL of the generated video (with audio baked in)
*/
export async function generateAudioVideo({
Expand All @@ -37,19 +42,12 @@ export async function generateAudioVideo({
motionPrompt: string;
}): Promise<string> {
const config = DEFAULT_PIPELINE_CONFIG;
const durationSeconds = Math.min(
audioDurationSeconds,
config.audioVideoModelMaxSeconds,
);
const durationSeconds = Math.min(audioDurationSeconds, config.audioVideoModelMaxSeconds);
const fps = 25;
const numFrames = Math.round(durationSeconds * fps) + 1;

// Clip the audio to the right section and upload to fal
const audioUrl = await clipAndUploadAudio(
songBuffer,
audioStartSeconds,
durationSeconds,
);
const audioUrl = await clipAndUploadAudio(songBuffer, audioStartSeconds, durationSeconds);

logger.log("Generating audio-to-video (lipsync)", {
model: config.audioVideoModel,
Expand Down Expand Up @@ -89,6 +87,10 @@ export async function generateAudioVideo({

/**
* Clips the song mp3 to the specified range and uploads to fal storage.
*
* @param songBuffer
* @param startSeconds
* @param durationSeconds
*/
async function clipAndUploadAudio(
songBuffer: Buffer,
Expand All @@ -105,10 +107,14 @@ async function clipAndUploadAudio(

await execFileAsync("ffmpeg", [
"-y",
"-i", inputPath,
"-ss", String(startSeconds),
"-t", String(durationSeconds),
"-c", "copy",
"-i",
inputPath,
"-ss",
String(startSeconds),
"-t",
String(durationSeconds),
"-c",
"copy",
clippedPath,
]);

Expand All @@ -128,6 +134,10 @@ async function clipAndUploadAudio(
}
}

/**
*
* @param data
*/
function extractFalUrl(data: Record<string, unknown>): string | undefined {
for (const key of ["image", "video"]) {
if (data[key] && typeof data[key] === "object") {
Expand Down
23 changes: 17 additions & 6 deletions src/content/generateCaption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type { SongLyrics } from "./transcribeSong";
import type { CaptionLength } from "../schemas/contentCreationSchema";

const CAPTION_LENGTH_INSTRUCTIONS: Record<CaptionLength, string> = {
short: "Write a SHORT caption (max 10 words). Punchy, minimal, like a text message. Think: one phrase that hits.",
medium: "Write a MEDIUM caption (15-30 words). A complete thought with feeling. 1-2 sentences max.",
short:
"Write a SHORT caption (max 10 words). Punchy, minimal, like a text message. Think: one phrase that hits.",
medium:
"Write a MEDIUM caption (15-30 words). A complete thought with feeling. 1-2 sentences max.",
long: "Write a LONG caption (40-80 words). A mini-story or stream of consciousness. Vulnerable, raw, the kind of caption people screenshot.",
};

Expand All @@ -14,6 +16,15 @@ const CAPTION_LENGTH_INSTRUCTIONS: Record<CaptionLength, string> = {
* Combines template style, artist context, song lyrics, and audience data.
*
* Matches the content-creation-app's generateCaption.ts behavior.
*
* @param root0
* @param root0.template
* @param root0.songTitle
* @param root0.fullLyrics
* @param root0.clipLyrics
* @param root0.artistContext
* @param root0.audienceContext
* @param root0.captionLength
Comment on lines +19 to +27
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

Auto-generated JSDoc parameter names are not meaningful.

The root0, root0.template, etc. parameter names appear to be auto-generated placeholders and don't provide useful documentation. Consider either removing them or replacing with meaningful descriptions.

📝 Suggested improvement
 * Matches the content-creation-app's generateCaption.ts behavior.
 *
- * `@param` root0
- * `@param` root0.template
- * `@param` root0.songTitle
- * `@param` root0.fullLyrics
- * `@param` root0.clipLyrics
- * `@param` root0.artistContext
- * `@param` root0.audienceContext
- * `@param` root0.captionLength
+ * `@param` options - Caption generation options
+ * `@param` options.template - Template data with style/caption guides
+ * `@param` options.songTitle - Title of the song
+ * `@param` options.fullLyrics - Complete song lyrics
+ * `@param` options.clipLyrics - Lyrics for the selected clip window
+ * `@param` options.artistContext - Context about the artist
+ * `@param` options.audienceContext - Context about the target audience
+ * `@param` options.captionLength - Desired caption length (short/medium/long)
 */
📝 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 root0
* @param root0.template
* @param root0.songTitle
* @param root0.fullLyrics
* @param root0.clipLyrics
* @param root0.artistContext
* @param root0.audienceContext
* @param root0.captionLength
*
* `@param` options - Caption generation options
* `@param` options.template - Template data with style/caption guides
* `@param` options.songTitle - Title of the song
* `@param` options.fullLyrics - Complete song lyrics
* `@param` options.clipLyrics - Lyrics for the selected clip window
* `@param` options.artistContext - Context about the artist
* `@param` options.audienceContext - Context about the target audience
* `@param` options.captionLength - Desired caption length (short/medium/long)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/generateCaption.ts` around lines 19 - 27, The JSDoc block above
generateCaption contains auto-generated param names like root0 and
root0.template; update the comment to use the actual parameter names (template,
songTitle, fullLyrics, clipLyrics, artistContext, audienceContext,
captionLength) and provide brief meaningful descriptions for each (or remove any
unused params), ensuring the JSDoc tags match the function signature in
generateCaption so IDEs and docs show correct parameter info.

*/
export async function generateCaption({
template,
Expand Down Expand Up @@ -41,9 +52,10 @@ export async function generateCaption({
? JSON.stringify(template.captionGuide, null, 2)
: "(no caption guide)";

const examples = template.captionExamples.length > 0
? template.captionExamples.map(c => `- "${c}"`).join("\n")
: "(no examples)";
const examples =
template.captionExamples.length > 0
? template.captionExamples.map(c => `- "${c}"`).join("\n")
: "(no examples)";

const lengthInstruction = CAPTION_LENGTH_INSTRUCTIONS[captionLength];

Expand Down Expand Up @@ -117,4 +129,3 @@ Generate ONE caption. ${lengthInstruction} Return ONLY the caption text, nothing
logger.log("Caption generated", { caption: captionText.slice(0, 80) });
return captionText;
}

9 changes: 8 additions & 1 deletion src/content/generateContentImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import { DEFAULT_PIPELINE_CONFIG } from "./defaultPipelineConfig";
* The prompt tells the model to replace the person in the reference scene
* with the person from the face-guide headshot.
*
* @param faceGuideUrl.faceGuideUrl
* @param faceGuideUrl - fal storage URL of the artist's face-guide (headshot)
* @param referenceImagePath - local path to a template reference image (or null)
* @param prompt - Scene/style prompt that instructs the face swap
* @param faceGuideUrl.referenceImagePath
* @param faceGuideUrl.prompt
Comment on lines +16 to +21
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

Malformed JSDoc @param annotations.

Same pattern as other files — faceGuideUrl.faceGuideUrl, faceGuideUrl.referenceImagePath, etc. are incorrectly formatted.

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

In `@src/content/generateContentImage.ts` around lines 16 - 21, The JSDoc `@param`
annotations in generateContentImage.ts are malformed (e.g., entries like
"faceGuideUrl.faceGuideUrl" and duplicated fields); update the JSDoc for the
function that accepts parameters faceGuideUrl, referenceImagePath, and prompt so
each `@param` line uses the simple parameter name and a short description (for
example "@param faceGuideUrl - fal storage URL of the artist's face-guide
(headshot)", "@param referenceImagePath - local path to a template reference
image or null", "@param prompt - scene/style prompt that instructs the face
swap"), remove the incorrect dotted entries (faceGuideUrl.faceGuideUrl,
faceGuideUrl.referenceImagePath, faceGuideUrl.prompt) and ensure the remaining
`@param` names exactly match the function signature.

* @returns URL of the generated image
*/
export async function generateContentImage({
Expand Down Expand Up @@ -75,7 +78,11 @@ export async function generateContentImage({
return imageUrl;
}

/** Extracts a media URL from various fal.ai response shapes. */
/**
* Extracts a media URL from various fal.ai response shapes.
*
* @param data
*/
function extractFalUrl(data: Record<string, unknown>): string | undefined {
for (const key of ["image", "video"]) {
if (data[key] && typeof data[key] === "object") {
Expand Down
9 changes: 5 additions & 4 deletions src/content/generateContentVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { DEFAULT_PIPELINE_CONFIG } from "./defaultPipelineConfig";
/**
* Generates a video from an AI-generated image using fal.ai image-to-video.
*
* @param imageUrl.imageUrl
* @param imageUrl - URL of the source image (from generateContentImage)
* @param motionPrompt - Describes how the subject should move
* @param imageUrl.motionPrompt
Comment on lines +8 to +11
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

Malformed JSDoc @param annotations.

Same issue as in generateAudioVideo.ts — the annotations incorrectly reference imageUrl.imageUrl and imageUrl.motionPrompt instead of documenting the actual destructured parameters.

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

In `@src/content/generateContentVideo.ts` around lines 8 - 11, Fix the malformed
JSDoc for the generateContentVideo function: replace the incorrect annotations
like `@param imageUrl.imageUrl` and `@param imageUrl.motionPrompt` with proper
param entries that match the actual destructured parameters (e.g., `@param
imageUrl - URL of the source image (from generateContentImage)` and `@param
motionPrompt - Describes how the subject should move`), remove any
duplicate/misnamed lines, and ensure the JSDoc matches the function signature
used in generateContentVideo.

* @returns URL of the generated video
*/
export async function generateContentVideo({
Expand All @@ -17,10 +19,7 @@ export async function generateContentVideo({
motionPrompt: string;
}): Promise<string> {
const config = DEFAULT_PIPELINE_CONFIG;
const durationSeconds = Math.min(
config.clipDuration,
config.videoModelMaxSeconds,
);
const durationSeconds = Math.min(config.clipDuration, config.videoModelMaxSeconds);

logger.log("Generating video", {
model: config.videoModel,
Expand Down Expand Up @@ -55,6 +54,8 @@ export async function generateContentVideo({

/**
* Extracts a media URL from various fal.ai response shapes.
*
* @param data
*/
function extractFalUrl(data: Record<string, unknown>): string | undefined {
for (const key of ["image", "video"]) {
Expand Down
Loading
Loading