-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/gateway unified analysis #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e1848ae
d7c1acc
b983433
a714302
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||
|
||||||||||
| const delta = (Math.random() - 0.5) * magnitude | |
| const hash = fnv1a(value.toString()) | |
| const rand = (hash % 1000000) / 1000000 | |
| const delta = (rand - 0.5) * magnitude |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) | ||
|
Comment on lines
+64
to
+65
|
||
| const featureScores = buildFeatureScores(seed) | ||
| const extension = getFileExtension(file.name) | ||
| const format = extension ? extension.slice(1).toUpperCase() : 'AUDIO' | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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)) | ||||||
|
Comment on lines
+124
to
+125
|
||||||
| const featureScores = buildFeatureScores(seed) | ||||||
|
|
||||||
| const indicators = previewIndicators() | ||||||
|
|
@@ -128,13 +136,21 @@ const buildPreviewResult = ( | |||||
| processingTime: elapsedSec, | ||||||
| modelVersion: 'youtube-preview-v1', | ||||||
|
||||||
| modelVersion: 'youtube-preview-v1', | |
| modelVersion: parsed.kind === 'spotify' ? 'spotify-preview-v1' : 'youtube-preview-v1', |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BackendResponse interface assumes the source field will always have a video_id property (line 28), but this field won't be present for Spotify tracks. When the backend returns a Spotify response, accessing response.source.video_id will be undefined, which could cause issues if this field is used elsewhere. Consider making video_id optional or adding a track_id field to handle Spotify responses properly.
| videoId: response.source.video_id, | |
| videoId: response.source.video_id ?? parsed.videoId, |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -205,7 +205,7 @@ const AIMusicDetectionPage: NextPage = () => { | |||||
| </div> | ||||||
|
|
||||||
| <div className={styles['result-source']}> | ||||||
| {source.kind === 'youtube' ? ( | ||||||
| {source.kind === 'youtube' && ( | ||||||
| <> | ||||||
| <div className={styles['result-source-item']}> | ||||||
| <span>{t.aiDetection.result.videoId}</span> | ||||||
|
|
@@ -216,7 +216,20 @@ const AIMusicDetectionPage: NextPage = () => { | |||||
| <span>{source.normalizedUrl}</span> | ||||||
| </div> | ||||||
| </> | ||||||
| ) : ( | ||||||
| )} | ||||||
| {source.kind === 'spotify' && ( | ||||||
| <> | ||||||
| <div className={styles['result-source-item']}> | ||||||
| <span>Spotify Track</span> | ||||||
|
||||||
| <span>Spotify Track</span> | |
| <span>{t.aiDetection.result.spotifyTrack}</span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling Netlify's secret scanning by setting
SECRETS_SCAN_ENABLED = "false"weakens your build-time defenses and increases the risk that accidentally committed credentials or other secrets pass through CI/CD undetected and end up exposed in artifacts or logs. An attacker who obtains access to the repository or build outputs would more easily discover usable secrets that would otherwise have been flagged and blocked. Unless there is a strong, documented justification and compensating controls, you should keep secret scanning enabled (or enforce an equivalent control elsewhere) to maintain protection against leaked credentials.