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/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/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/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..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,27 +102,27 @@ 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 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() @@ -128,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 @@ -143,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 || [] @@ -166,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] @@ -183,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') } } } @@ -197,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) @@ -220,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)) @@ -243,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/netlify/functions/analyze.js b/platform/netlify/functions/analyze.js similarity index 100% rename from netlify/functions/analyze.js rename to platform/netlify/functions/analyze.js 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}