diff --git a/netlify.toml b/netlify.toml index 6c4fca7..828ab2d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -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" diff --git a/netlify/functions/analyze.js b/netlify/functions/analyze.js new file mode 100644 index 0000000..eb72cfc --- /dev/null +++ b/netlify/functions/analyze.js @@ -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') + +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.' + ] + }, + audioInfo: { + duration: 0, + sampleRate: 44100, + bitrate: 192, + format: 'PREVIEW' + } + } + + return { + statusCode: 200, + body: JSON.stringify({ result, warnings: ['gateway_mock_preview'] }) + } +} diff --git a/platform/hooks/analysisGateway.ts b/platform/hooks/analysisGateway.ts new file mode 100644 index 0000000..2a6fe5e --- /dev/null +++ b/platform/hooks/analysisGateway.ts @@ -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 + +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 + }) + + if (!response.ok) { + return { result: null, error: 'backend_unreachable' as AnalysisErrorCode } + } + + const data = await response.json() as AnalyzeResponse + 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 } +} diff --git a/platform/hooks/analysisTypes.ts b/platform/hooks/analysisTypes.ts index 18e5cfc..d986285 100644 --- a/platform/hooks/analysisTypes.ts +++ b/platform/hooks/analysisTypes.ts @@ -52,3 +52,6 @@ export type AnalysisErrorCode = | 'fileTooLarge' | 'fileTooSmall' | 'invalidFileName' + | 'backend_not_configured' + | 'backend_unreachable' + | 'backend_unexpected_response' diff --git a/platform/hooks/analysisUtils.ts b/platform/hooks/analysisUtils.ts index 5e1b470..d69f4c5 100644 --- a/platform/hooks/analysisUtils.ts +++ b/platform/hooks/analysisUtils.ts @@ -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 +} diff --git a/platform/hooks/useFileAnalysis.ts b/platform/hooks/useFileAnalysis.ts index 1b705cf..401c7b8 100644 --- a/platform/hooks/useFileAnalysis.ts +++ b/platform/hooks/useFileAnalysis.ts @@ -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 @@ -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, diff --git a/platform/hooks/useYouTubeAnalysis.ts b/platform/hooks/useYouTubeAnalysis.ts index 9bde39b..c171f72 100644 --- a/platform/hooks/useYouTubeAnalysis.ts +++ b/platform/hooks/useYouTubeAnalysis.ts @@ -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 @@ -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.') }