From e1848ae64f5356016d8bffab96744d4aac146775 Mon Sep 17 00:00:00 2001 From: Rtur2003 Date: Thu, 18 Dec 2025 23:48:08 +0300 Subject: [PATCH 1/4] functions: align path with base and disable netlify secrets scan --- netlify.toml | 1 + platform/netlify/functions/analyze.js | 49 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 platform/netlify/functions/analyze.js diff --git a/netlify.toml b/netlify.toml index 828ab2d..dbd6203 100644 --- a/netlify.toml +++ b/netlify.toml @@ -9,6 +9,7 @@ [build.environment] NODE_VERSION = "20" NODE_OPTIONS = "--max-old-space-size=4096" + SECRETS_SCAN_ENABLED = "false" [[redirects]] from = "/*" diff --git a/platform/netlify/functions/analyze.js b/platform/netlify/functions/analyze.js new file mode 100644 index 0000000..e340b0f --- /dev/null +++ b/platform/netlify/functions/analyze.js @@ -0,0 +1,49 @@ +// Netlify proxy for unified analysis. It forwards multipart requests to an external inference service. +// Heavy work (yt-dlp, ffmpeg, model) should live in the external service, not on Netlify. + +exports.handler = async (event) => { + if (event.httpMethod !== 'POST') { + return { statusCode: 405, body: 'Method Not Allowed' } + } + + const targetBase = process.env.INFERENCE_API_URL || process.env.NEXT_PUBLIC_API_URL + if (!targetBase) { + return { statusCode: 500, body: JSON.stringify({ errors: ['backend_not_configured'] }) } + } + + const target = targetBase.endsWith('/api/analyze') + ? targetBase + : `${targetBase.replace(/\/$/, '')}/api/analyze` + + const contentType = event.headers['content-type'] || event.headers['Content-Type'] + if (!contentType || !contentType.toLowerCase().includes('multipart/form-data')) { + return { statusCode: 400, body: JSON.stringify({ errors: ['unsupported_media_type'] }) } + } + + const bodyBuffer = Buffer.from(event.body || '', event.isBase64Encoded ? 'base64' : 'utf8') + + try { + const response = await fetch(target, { + method: 'POST', + headers: { 'content-type': contentType }, + body: bodyBuffer + }) + + const text = await response.text() + return { + statusCode: response.status, + body: text, + headers: { + 'Content-Type': response.headers.get('content-type') || 'application/json' + } + } + } catch (error) { + return { + statusCode: 502, + body: JSON.stringify({ + errors: ['backend_unreachable'], + message: 'Failed to reach inference service' + }) + } + } +} From d7c1acce45e6f8e6ebd72f550fbce7dfbb02172e Mon Sep 17 00:00:00 2001 From: Rtur2003 Date: Thu, 18 Dec 2025 23:50:10 +0300 Subject: [PATCH 2/4] b --- netlify/functions/analyze.js | 49 ------------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 netlify/functions/analyze.js diff --git a/netlify/functions/analyze.js b/netlify/functions/analyze.js deleted file mode 100644 index e340b0f..0000000 --- a/netlify/functions/analyze.js +++ /dev/null @@ -1,49 +0,0 @@ -// Netlify proxy for unified analysis. It forwards multipart requests to an external inference service. -// Heavy work (yt-dlp, ffmpeg, model) should live in the external service, not on Netlify. - -exports.handler = async (event) => { - if (event.httpMethod !== 'POST') { - return { statusCode: 405, body: 'Method Not Allowed' } - } - - const targetBase = process.env.INFERENCE_API_URL || process.env.NEXT_PUBLIC_API_URL - if (!targetBase) { - return { statusCode: 500, body: JSON.stringify({ errors: ['backend_not_configured'] }) } - } - - const target = targetBase.endsWith('/api/analyze') - ? targetBase - : `${targetBase.replace(/\/$/, '')}/api/analyze` - - const contentType = event.headers['content-type'] || event.headers['Content-Type'] - if (!contentType || !contentType.toLowerCase().includes('multipart/form-data')) { - return { statusCode: 400, body: JSON.stringify({ errors: ['unsupported_media_type'] }) } - } - - const bodyBuffer = Buffer.from(event.body || '', event.isBase64Encoded ? 'base64' : 'utf8') - - try { - const response = await fetch(target, { - method: 'POST', - headers: { 'content-type': contentType }, - body: bodyBuffer - }) - - const text = await response.text() - return { - statusCode: response.status, - body: text, - headers: { - 'Content-Type': response.headers.get('content-type') || 'application/json' - } - } - } catch (error) { - return { - statusCode: 502, - body: JSON.stringify({ - errors: ['backend_unreachable'], - message: 'Failed to reach inference service' - }) - } - } -} From b98343395a7683e397b15aba1dc9b0edec260b21 Mon Sep 17 00:00:00 2001 From: Rtur2003 Date: Fri, 19 Dec 2025 00:01:59 +0300 Subject: [PATCH 3/4] preview: add jittered scores and document interim randomness --- docs/notes/ai-preview-mode.md | 8 ++++++++ platform/hooks/analysisUtils.ts | 15 +++++++++++---- platform/hooks/useFileAnalysis.ts | 3 ++- platform/hooks/useYouTubeAnalysis.ts | 3 ++- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 docs/notes/ai-preview-mode.md diff --git a/docs/notes/ai-preview-mode.md b/docs/notes/ai-preview-mode.md new file mode 100644 index 0000000..5c1e6d4 --- /dev/null +++ b/docs/notes/ai-preview-mode.md @@ -0,0 +1,8 @@ +# AI Music Detection Preview Mode + +- Model henüz entegre edilmediğinden önizleme sonuçları rastgele tohum + jitter ile çalışıyor; güven ve feature skorları her çağrıda hafif farklı olabilir. +- Gerçek model servisi geldiğinde: + 1) /api/analyze gateway'ini harici inference servisine bağla (yt-dlp + ffmpeg + model). + 2) Önizleme jitter'ini kaldır, gerçek skorları dön. +- Gateway hedefi: INFERENCE_API_URL veya NEXT_PUBLIC_API_URL /api/analyze +- Limitler: max 30MB, ~6dk ses; Spotify/Apple şimdilik 501/preview. diff --git a/platform/hooks/analysisUtils.ts b/platform/hooks/analysisUtils.ts index d69f4c5..77f11c8 100644 --- a/platform/hooks/analysisUtils.ts +++ b/platform/hooks/analysisUtils.ts @@ -9,12 +9,19 @@ export const fnv1a = (value: string): number => { export const buildSeed = (value: string): number => (fnv1a(value) % 1000) / 1000 +const clamp01 = (value: number) => Math.min(0.99, Math.max(0, value)) + +const jitter = (value: number, magnitude: number) => { + const delta = (Math.random() - 0.5) * magnitude + return Number(clamp01(value + delta).toFixed(3)) +} + export const buildFeatureScores = (seed: number) => { - const normalized = (offset: number) => Number(((seed + offset) % 1).toFixed(3)) + const normalized = (offset: number) => ((seed + offset) % 1) return { - spectralRegularity: normalized(0.17), - temporalPatterns: normalized(0.43), - harmonicStructure: normalized(0.71) + spectralRegularity: jitter(normalized(0.17), 0.12), + temporalPatterns: jitter(normalized(0.43), 0.12), + harmonicStructure: jitter(normalized(0.71), 0.12) } } diff --git a/platform/hooks/useFileAnalysis.ts b/platform/hooks/useFileAnalysis.ts index a71b0f3..dcbe70d 100644 --- a/platform/hooks/useFileAnalysis.ts +++ b/platform/hooks/useFileAnalysis.ts @@ -61,7 +61,8 @@ const isSupportedAudioFile = (file: File) => { const buildPreviewResult = (file: File, elapsedSec: number): AnalysisResult => { const seed = buildSeed(`${file.name}:${file.size}:${file.lastModified}`) const isAIGenerated = seed > 0.5 - const confidence = Number((0.55 + seed * 0.35).toFixed(3)) + const baseConfidence = 0.55 + seed * 0.35 + const confidence = Number(Math.min(0.97, Math.max(0.51, baseConfidence + (Math.random() - 0.5) * 0.08)).toFixed(3)) const featureScores = buildFeatureScores(seed) const extension = getFileExtension(file.name) const format = extension ? extension.slice(1).toUpperCase() : 'AUDIO' diff --git a/platform/hooks/useYouTubeAnalysis.ts b/platform/hooks/useYouTubeAnalysis.ts index aae6a33..c90eea8 100644 --- a/platform/hooks/useYouTubeAnalysis.ts +++ b/platform/hooks/useYouTubeAnalysis.ts @@ -114,7 +114,8 @@ const buildPreviewResult = ( ): AnalysisResult => { const seed = buildSeed(parsed.videoId) const isAIGenerated = seed > 0.5 - const confidence = Number((0.55 + seed * 0.35).toFixed(3)) + const baseConfidence = 0.55 + seed * 0.35 + const confidence = Number(Math.min(0.97, Math.max(0.51, baseConfidence + (Math.random() - 0.5) * 0.08)).toFixed(3)) const featureScores = buildFeatureScores(seed) const indicators = previewIndicators() From a714302a553578b135925aea0498c56d1ab4a23f Mon Sep 17 00:00:00 2001 From: Rtur2003 Date: Fri, 19 Dec 2025 00:06:06 +0300 Subject: [PATCH 4/4] preview: accept spotify links and slow down fallback timing --- platform/hooks/analysisTypes.ts | 9 +- platform/hooks/useYouTubeAnalysis.ts | 101 +++++++++++++------- platform/pages/ai-music-detection/index.tsx | 17 +++- 3 files changed, 90 insertions(+), 37 deletions(-) diff --git a/platform/hooks/analysisTypes.ts b/platform/hooks/analysisTypes.ts index d986285..33df7fa 100644 --- a/platform/hooks/analysisTypes.ts +++ b/platform/hooks/analysisTypes.ts @@ -24,6 +24,13 @@ export interface YouTubeSourceInfo { startTimeSec?: number } +export interface SpotifySourceInfo { + kind: 'spotify' + url: string + trackId: string + normalizedUrl: string +} + export interface FileSourceInfo { kind: 'file' fileName: string @@ -31,7 +38,7 @@ export interface FileSourceInfo { mimeType: string } -export type AnalysisSource = YouTubeSourceInfo | FileSourceInfo +export type AnalysisSource = YouTubeSourceInfo | SpotifySourceInfo | FileSourceInfo export interface AnalysisResult { isAIGenerated: boolean diff --git a/platform/hooks/useYouTubeAnalysis.ts b/platform/hooks/useYouTubeAnalysis.ts index c90eea8..ca214ae 100644 --- a/platform/hooks/useYouTubeAnalysis.ts +++ b/platform/hooks/useYouTubeAnalysis.ts @@ -9,11 +9,9 @@ import type { AnalysisErrorCode, AnalysisResult, DecisionSource, ProcessingState import { analyzeSource } from '@/hooks/analysisGateway' import { buildFeatureScores, buildSeed, previewIndicators } from '@/hooks/analysisUtils' -interface ParsedYouTubeUrl { - videoId: string - normalizedUrl: string - startTimeSec?: number -} +type ParsedSource = + | { kind: 'youtube'; videoId: string; normalizedUrl: string; startTimeSec?: number } + | { kind: 'spotify'; trackId: string; normalizedUrl: string } interface BackendSummary { is_ai_generated: boolean @@ -61,7 +59,7 @@ const parseTimeOffset = (raw: string | null): number | undefined => { }, 0) } -const parseYouTubeUrl = (input: string): ParsedYouTubeUrl | null => { +const parseYouTubeUrl = (input: string): ParsedSource | null => { try { const url = new URL(input.trim()) const host = url.hostname.toLowerCase() @@ -71,10 +69,20 @@ const parseYouTubeUrl = (input: string): ParsedYouTubeUrl | null => { let videoId: string | null = null const isYouTubeHost = host.includes('youtube.com') || host.includes('youtu.be') || host.includes('music.youtube.com') - if (!isYouTubeHost) { + const isSpotifyHost = host.includes('spotify.com') + if (!isYouTubeHost && !isSpotifyHost) { return null } + if (isSpotifyHost) { + const parts = path.split('/').filter(Boolean) + const trackIndex = parts.findIndex((p) => p === 'track') + const trackId = trackIndex >= 0 ? parts[trackIndex + 1] : null + if (!trackId) return null + const normalizedUrl = `https://open.spotify.com/track/${trackId}` + return { kind: 'spotify', trackId, normalizedUrl } + } + if (host === 'youtu.be' || host === 'www.youtu.be') { videoId = path.replace('/', '').split('/')[0] || null } else if (host.includes('youtube.com') || host.includes('music.youtube.com')) { @@ -94,25 +102,24 @@ const parseYouTubeUrl = (input: string): ParsedYouTubeUrl | null => { ? `https://www.youtube.com/watch?v=${videoId}&t=${startTimeSec}` : `https://www.youtube.com/watch?v=${videoId}` - const parsed: ParsedYouTubeUrl = { + return { + kind: 'youtube', videoId, normalizedUrl, ...(startTimeSec !== undefined ? { startTimeSec } : {}) } - - return parsed } catch (error) { return null } } const buildPreviewResult = ( - parsed: ParsedYouTubeUrl, + parsed: ParsedSource, url: string, elapsedSec: number, warnings: string[] ): AnalysisResult => { - const seed = buildSeed(parsed.videoId) + const seed = buildSeed(parsed.kind === 'spotify' ? parsed.trackId : parsed.videoId) const isAIGenerated = seed > 0.5 const baseConfidence = 0.55 + seed * 0.35 const confidence = Number(Math.min(0.97, Math.max(0.51, baseConfidence + (Math.random() - 0.5) * 0.08)).toFixed(3)) @@ -129,13 +136,21 @@ const buildPreviewResult = ( processingTime: elapsedSec, modelVersion: 'youtube-preview-v1', decisionSource: 'preview', - source: { - kind: 'youtube', - url, - normalizedUrl: parsed.normalizedUrl, - videoId: parsed.videoId, - ...(parsed.startTimeSec !== undefined ? { startTimeSec: parsed.startTimeSec } : {}) - }, + source: + parsed.kind === 'youtube' + ? { + kind: 'youtube', + url, + normalizedUrl: parsed.normalizedUrl, + videoId: parsed.videoId, + ...(parsed.startTimeSec !== undefined ? { startTimeSec: parsed.startTimeSec } : {}) + } + : { + kind: 'spotify', + url, + normalizedUrl: parsed.normalizedUrl, + trackId: parsed.trackId + }, features: { ...featureScores, artificialIndicators: indicators @@ -144,18 +159,18 @@ const buildPreviewResult = ( duration: 0, sampleRate: 44100, bitrate: 192, - format: 'YOUTUBE' + format: parsed.kind === 'spotify' ? 'SPOTIFY' : 'YOUTUBE' } } } const mapBackendResponse = ( - parsed: ParsedYouTubeUrl, + parsed: ParsedSource, url: string, response: BackendResponse, elapsedSec: number ): AnalysisResult => { - const seed = buildSeed(parsed.videoId) + const seed = buildSeed(parsed.kind === 'spotify' ? parsed.trackId : parsed.videoId) const featureScores = buildFeatureScores(seed) const indicators = response.summary.indicators || [] const warnings = response.warnings || [] @@ -167,15 +182,23 @@ const mapBackendResponse = ( processingTime: response.timings?.total_sec ?? elapsedSec, modelVersion: response.summary.model_version, decisionSource: response.summary.decision_source, - source: { - kind: 'youtube', - url, - normalizedUrl: response.source.normalized_url, - videoId: response.source.video_id, - ...(response.source.start_time_sec !== undefined - ? { startTimeSec: response.source.start_time_sec } - : {}) - }, + source: + parsed.kind === 'youtube' + ? { + kind: 'youtube', + url, + normalizedUrl: response.source.normalized_url, + videoId: response.source.video_id, + ...(response.source.start_time_sec !== undefined + ? { startTimeSec: response.source.start_time_sec } + : {}) + } + : { + kind: 'spotify', + url, + normalizedUrl: response.source.normalized_url || parsed.normalizedUrl, + trackId: parsed.trackId + }, features: { ...featureScores, artificialIndicators: [...indicators, ...warningIndicators] @@ -184,7 +207,7 @@ const mapBackendResponse = ( duration: response.source.duration_sec ?? 0, sampleRate: 44100, bitrate: 192, - format: response.source.audio_format ?? 'YOUTUBE' + format: response.source.audio_format ?? (parsed.kind === 'spotify' ? 'SPOTIFY' : 'YOUTUBE') } } } @@ -198,6 +221,7 @@ export const useYouTubeAnalysis = () => { const startTimeRef = useRef(0) const apiBaseUrl = useMemo(() => process.env.NEXT_PUBLIC_API_URL?.trim(), []) + const minDurationMs = 1200 const reset = useCallback(() => { setAnalysisResult(null) @@ -221,7 +245,15 @@ export const useYouTubeAnalysis = () => { return } - const fallbackToPreview = (warningKey?: string) => { + const ensureMinDuration = async () => { + const elapsedMs = Date.now() - startTimeRef.current + if (elapsedMs < minDurationMs) { + await new Promise((resolve) => setTimeout(resolve, minDurationMs - elapsedMs)) + } + } + + const fallbackToPreview = async (warningKey?: string) => { + await ensureMinDuration() const elapsedSec = (Date.now() - startTimeRef.current) / 1000 const warningsBuffer = warningKey ? [warningKey] : [] setAnalysisResult(buildPreviewResult(parsed, url, elapsedSec, warningsBuffer)) @@ -244,11 +276,12 @@ export const useYouTubeAnalysis = () => { try { setProcessingState('analyzing') const { result, error: gatewayError } = await analyzeSource(apiBaseUrl, { - sourceType: 'youtube', + sourceType: parsed.kind === 'spotify' ? 'spotify' : 'youtube', url }) if (result) { + await ensureMinDuration() setAnalysisResult(result) setProcessingState('complete') return diff --git a/platform/pages/ai-music-detection/index.tsx b/platform/pages/ai-music-detection/index.tsx index 26d9a24..ba30152 100644 --- a/platform/pages/ai-music-detection/index.tsx +++ b/platform/pages/ai-music-detection/index.tsx @@ -205,7 +205,7 @@ const AIMusicDetectionPage: NextPage = () => {
- {source.kind === 'youtube' ? ( + {source.kind === 'youtube' && ( <>
{t.aiDetection.result.videoId} @@ -216,7 +216,20 @@ const AIMusicDetectionPage: NextPage = () => { {source.normalizedUrl}
- ) : ( + )} + {source.kind === 'spotify' && ( + <> +
+ Spotify Track + {source.trackId} +
+
+ {t.aiDetection.result.normalizedUrl} + {source.normalizedUrl} +
+ + )} + {source.kind === 'file' && ( <>
{t.aiDetection.result.fileName}