Skip to content
Merged
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
3 changes: 3 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
command = "corepack prepare npm@10.9.2 --activate && npm install --prefix . && npm run build --prefix ."
publish = "out"

[functions]
directory = "netlify/functions"

[build.environment]
NODE_VERSION = "20"
NODE_OPTIONS = "--max-old-space-size=4096"
Expand Down
49 changes: 49 additions & 0 deletions netlify/functions/analyze.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Unified analysis gateway (mock/preview). Real integrations (yt-dlp/model) should be wired to the providers.
const { promises: fs } = require('fs')
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The imported fs module is never used in this function. Consider removing this unused import to keep the code clean.

Suggested change
const { promises: fs } = require('fs')

Copilot uses AI. Check for mistakes.

exports.handler = async (event) => {
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' }
}

// NOTE: Netlify Functions cannot parse multipart by default; here we return a fixed preview response.
// In real usage, move this to a full server or add multipart parsing + external service calls.
const now = Date.now()
const seed = (now % 1000) / 1000
const isAIGenerated = seed > 0.5
const confidence = Number((0.55 + seed * 0.35).toFixed(3))

const result = {
isAIGenerated,
confidence,
processingTime: 0.4,
modelVersion: 'gateway-preview-v1',
decisionSource: 'preview',
source: {
kind: 'file',
fileName: 'preview',
fileSizeBytes: 0,
mimeType: 'application/octet-stream'
},
features: {
spectralRegularity: Number(((seed + 0.17) % 1).toFixed(3)),
temporalPatterns: Number(((seed + 0.43) % 1).toFixed(3)),
harmonicStructure: Number(((seed + 0.71) % 1).toFixed(3)),
artificialIndicators: [
'Preview-only decision based on fingerprint.',
'No model inference was available at request time.'
]
Comment on lines +32 to +35
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The hardcoded preview indicator messages in the Netlify function are inconsistent with the previewIndicators() utility function used elsewhere in the codebase. The Netlify function uses "Preview-only decision based on fingerprint." while the utility in analysisUtils.ts returns the same message. However, this creates maintenance burden - if the preview indicator messages need to change, they must be updated in two places. Consider importing and using the previewIndicators() utility function, or if that's not possible in this Node.js context, document this duplication and the reason for it.

Copilot uses AI. Check for mistakes.
},
audioInfo: {
duration: 0,
sampleRate: 44100,
bitrate: 192,
format: 'PREVIEW'
}
}

return {
statusCode: 200,
body: JSON.stringify({ result, warnings: ['gateway_mock_preview'] })
}
}
59 changes: 59 additions & 0 deletions platform/hooks/analysisGateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { AnalysisResult, AnalysisErrorCode } from '@/hooks/analysisTypes'

export type SourceType = 'youtube' | 'file' | 'spotify' | 'apple'

export interface AnalyzePayload {
sourceType: SourceType
url?: string
file?: File
}

export interface AnalyzeResponse {
result?: AnalysisResult
warnings?: string[]
errors?: string[]
}

const MAX_BYTES = 30 * 1024 * 1024 // 30MB
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The MAX_BYTES constant is set to 30MB here, but in useFileAnalysis.ts, the MAX_FILE_SIZE_BYTES constant is set to 100MB. This inconsistency means that files between 30MB and 100MB would be rejected by the gateway but accepted by the frontend validation. These limits should be aligned to prevent confusing user experiences where a file passes frontend validation but fails at the backend.

Suggested change
const MAX_BYTES = 30 * 1024 * 1024 // 30MB
const MAX_BYTES = 100 * 1024 * 1024 // 100MB

Copilot uses AI. Check for mistakes.

export const analyzeSource = async (
apiBaseUrl: string | undefined,
payload: AnalyzePayload
): Promise<{ result: AnalysisResult | null; error: AnalysisErrorCode | null }> => {
if (!apiBaseUrl) {
return { result: null, error: 'backend_not_configured' as AnalysisErrorCode }
}

const formData = new FormData()
formData.append('sourceType', payload.sourceType)

if (payload.url) {
formData.append('url', payload.url)
}

if (payload.file) {
if (payload.file.size > MAX_BYTES) {
return { result: null, error: 'fileTooLarge' }
}
formData.append('file', payload.file)
}

const response = await fetch(`${apiBaseUrl}/api/analyze`, {
method: 'POST',
body: formData
})
Comment on lines +41 to +44
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The fetch call does not handle network errors or exceptions. If the fetch fails due to network issues (e.g., no internet connection, DNS failure), this will throw an unhandled exception. Consider wrapping the fetch in a try-catch block to properly handle these errors and return an appropriate error code like 'backend_unreachable'.

Suggested change
const response = await fetch(`${apiBaseUrl}/api/analyze`, {
method: 'POST',
body: formData
})
let response: Response
try {
response = await fetch(`${apiBaseUrl}/api/analyze`, {
method: 'POST',
body: formData
})
} catch {
return { result: null, error: 'backend_unreachable' as AnalysisErrorCode }
}

Copilot uses AI. Check for mistakes.

if (!response.ok) {
return { result: null, error: 'backend_unreachable' as AnalysisErrorCode }
Comment on lines +41 to +47
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The error handling for non-OK HTTP responses is too broad. A non-OK response could be a 400 (bad request), 500 (server error), or network timeout, but all are mapped to 'backend_unreachable'. Consider checking response.status to provide more specific error codes. For example, 4xx errors could map to different error codes than 5xx errors, improving the user experience with more accurate error messages.

Suggested change
const response = await fetch(`${apiBaseUrl}/api/analyze`, {
method: 'POST',
body: formData
})
if (!response.ok) {
return { result: null, error: 'backend_unreachable' as AnalysisErrorCode }
let response: Response
try {
response = await fetch(`${apiBaseUrl}/api/analyze`, {
method: 'POST',
body: formData
})
} catch {
// Network-level error (timeout, DNS failure, etc.)
return { result: null, error: 'backend_unreachable' as AnalysisErrorCode }
}
if (!response.ok) {
// Distinguish between client and server errors for better error reporting
if (response.status >= 400 && response.status < 500) {
return { result: null, error: 'backend_unexpected_response' as AnalysisErrorCode }
}
if (response.status >= 500 && response.status < 600) {
return { result: null, error: 'backend_unreachable' as AnalysisErrorCode }
}
return { result: null, error: 'backend_unexpected_response' as AnalysisErrorCode }

Copilot uses AI. Check for mistakes.
}

const data = await response.json() as AnalyzeResponse
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

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

The JSON parsing on line 50 could throw an exception if the response body is not valid JSON. Consider wrapping this in a try-catch block or handling the case where the response is malformed, returning 'backend_unexpected_response' error in such cases.

Suggested change
const data = await response.json() as AnalyzeResponse
let data: AnalyzeResponse
try {
data = (await response.json()) as AnalyzeResponse
} catch {
return { result: null, error: 'backend_unexpected_response' as AnalysisErrorCode }
}

Copilot uses AI. Check for mistakes.
if (data.errors && data.errors.length) {
// Map a few known errors to the existing codes
if (data.errors.includes('missing_file')) return { result: null, error: 'missingFile' }
if (data.errors.includes('unsupported_source')) return { result: null, error: 'invalidYouTubeUrl' }
return { result: null, error: 'backend_unexpected_response' as AnalysisErrorCode }
}

return { result: data.result ?? null, error: null }
}
3 changes: 3 additions & 0 deletions platform/hooks/analysisTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ export type AnalysisErrorCode =
| 'fileTooLarge'
| 'fileTooSmall'
| 'invalidFileName'
| 'backend_not_configured'
| 'backend_unreachable'
| 'backend_unexpected_response'
8 changes: 8 additions & 0 deletions platform/hooks/analysisUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ export const buildFeatureScores = (seed: number) => {
harmonicStructure: normalized(0.71)
}
}

export const previewIndicators = (extra?: string[]) => {
const base = [
'Preview-only decision based on fingerprint.',
'No model inference was available at request time.'
]
return extra && extra.length ? [...base, ...extra] : base
}
7 changes: 2 additions & 5 deletions platform/hooks/useFileAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { useCallback, useRef, useState } from 'react'
import type { AnalysisErrorCode, AnalysisResult, ProcessingState } from '@/hooks/analysisTypes'
import { buildFeatureScores, buildSeed } from '@/hooks/analysisUtils'
import { buildFeatureScores, buildSeed, previewIndicators } from '@/hooks/analysisUtils'

const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024
const MIN_FILE_SIZE_BYTES = 1024
Expand Down Expand Up @@ -79,10 +79,7 @@ const buildPreviewResult = (file: File, elapsedSec: number): AnalysisResult => {
},
features: {
...featureScores,
artificialIndicators: [
'Preview-only decision based on file fingerprint.',
'No model inference was available at request time.'
]
artificialIndicators: previewIndicators()
},
audioInfo: {
duration: 0,
Expand Down
7 changes: 2 additions & 5 deletions platform/hooks/useYouTubeAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { useCallback, useMemo, useRef, useState } from 'react'
import type { AnalysisErrorCode, AnalysisResult, DecisionSource, ProcessingState } from '@/hooks/analysisTypes'
import { buildFeatureScores, buildSeed } from '@/hooks/analysisUtils'
import { buildFeatureScores, buildSeed, previewIndicators } from '@/hooks/analysisUtils'

interface ParsedYouTubeUrl {
videoId: string
Expand Down Expand Up @@ -111,10 +111,7 @@ const buildPreviewResult = (
const confidence = Number((0.55 + seed * 0.35).toFixed(3))
const featureScores = buildFeatureScores(seed)

const indicators = [
'Preview-only decision based on URL fingerprint.',
'No model inference was available at request time.'
]
const indicators = previewIndicators()
if (warnings.length) {
indicators.push('Warnings reported by the backend pipeline.')
}
Expand Down
Loading