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
8 changes: 8 additions & 0 deletions docs/notes/ai-preview-mode.md
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.
1 change: 1 addition & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[build.environment]
NODE_VERSION = "20"
NODE_OPTIONS = "--max-old-space-size=4096"
SECRETS_SCAN_ENABLED = "false"
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.

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.

Suggested change
SECRETS_SCAN_ENABLED = "false"

Copilot uses AI. Check for mistakes.

[[redirects]]
from = "/*"
Expand Down
9 changes: 8 additions & 1 deletion platform/hooks/analysisTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,21 @@ export interface YouTubeSourceInfo {
startTimeSec?: number
}

export interface SpotifySourceInfo {
kind: 'spotify'
url: string
trackId: string
normalizedUrl: string
}

export interface FileSourceInfo {
kind: 'file'
fileName: string
fileSizeBytes: number
mimeType: string
}

export type AnalysisSource = YouTubeSourceInfo | FileSourceInfo
export type AnalysisSource = YouTubeSourceInfo | SpotifySourceInfo | FileSourceInfo

export interface AnalysisResult {
isAIGenerated: boolean
Expand Down
15 changes: 11 additions & 4 deletions platform/hooks/analysisUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 jitter function uses Math.random() which produces non-deterministic results. This means feature scores will vary on each analysis of the same source, breaking the deterministic seed-based approach. According to the PR description, preview mode should use "rastgele tohum + jitter" but the jitter makes results inconsistent between runs. If varied results on each call are desired, this is working as intended. However, if consistent results per source are expected (as suggested by the deterministic seed), consider using a seeded random generator for jitter as well.

Suggested change
const delta = (Math.random() - 0.5) * magnitude
const hash = fnv1a(value.toString())
const rand = (hash % 1000000) / 1000000
const delta = (rand - 0.5) * magnitude

Copilot uses AI. Check for mistakes.
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)
}
}

Expand Down
3 changes: 2 additions & 1 deletion platform/hooks/useFileAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 confidence calculation uses Math.random() which produces non-deterministic results. This means the same file analyzed twice will show different confidence values, even though the preview mode is supposed to simulate consistent results based on a deterministic seed. Consider using a seeded random function or removing randomness from the confidence calculation to maintain consistency with the deterministic approach used for feature scores.

Copilot uses AI. Check for mistakes.
const featureScores = buildFeatureScores(seed)
const extension = getFileExtension(file.name)
const format = extension ? extension.slice(1).toUpperCase() : 'AUDIO'
Expand Down
104 changes: 69 additions & 35 deletions platform/hooks/useYouTubeAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
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.

Spotify track URLs may include query parameters (e.g., ?si=...) that should be stripped when extracting the track ID. The current implementation extracts everything after '/track/' without removing query parameters, which could result in an invalid track ID. Consider using url.searchParams or splitting on '?' to extract only the track ID portion.

Copilot uses AI. Check for mistakes.
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')) {
Expand All @@ -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
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 confidence calculation uses Math.random() which produces non-deterministic results. This means the same track/video analyzed twice will show different confidence values, even though the preview mode is supposed to simulate consistent results based on a deterministic seed. Consider using a seeded random function or removing randomness from the confidence calculation to maintain consistency with the deterministic approach used for feature scores.

Copilot uses AI. Check for mistakes.
const featureScores = buildFeatureScores(seed)

const indicators = previewIndicators()
Expand All @@ -128,13 +136,21 @@ const buildPreviewResult = (
processingTime: elapsedSec,
modelVersion: 'youtube-preview-v1',
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 modelVersion is hardcoded as 'youtube-preview-v1' even for Spotify tracks. Consider using a more generic name like 'preview-v1' or dynamically setting it based on the source type to accurately reflect what's being analyzed.

Suggested change
modelVersion: 'youtube-preview-v1',
modelVersion: parsed.kind === 'spotify' ? 'spotify-preview-v1' : 'youtube-preview-v1',

Copilot uses AI. Check for mistakes.
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
Expand All @@ -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 || []
Expand All @@ -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,
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 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.

Suggested change
videoId: response.source.video_id,
videoId: response.source.video_id ?? parsed.videoId,

Copilot uses AI. Check for mistakes.
...(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]
Expand All @@ -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')
}
}
}
Expand All @@ -197,6 +221,7 @@ export const useYouTubeAnalysis = () => {
const startTimeRef = useRef<number>(0)

const apiBaseUrl = useMemo(() => process.env.NEXT_PUBLIC_API_URL?.trim(), [])
const minDurationMs = 1200

const reset = useCallback(() => {
setAnalysisResult(null)
Expand All @@ -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))
Expand All @@ -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
Expand Down
File renamed without changes.
17 changes: 15 additions & 2 deletions platform/pages/ai-music-detection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
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 label "Spotify Track" is hardcoded in English and doesn't use the translation system like other labels in this section. This breaks internationalization support. Consider adding a translation key such as t.aiDetection.result.spotifyTrack to maintain consistency with the YouTube videoId field on line 211.

Suggested change
<span>Spotify Track</span>
<span>{t.aiDetection.result.spotifyTrack}</span>

Copilot uses AI. Check for mistakes.
<span>{source.trackId}</span>
</div>
<div className={styles['result-source-item']}>
<span>{t.aiDetection.result.normalizedUrl}</span>
<span>{source.normalizedUrl}</span>
</div>
</>
)}
{source.kind === 'file' && (
<>
<div className={styles['result-source-item']}>
<span>{t.aiDetection.result.fileName}</span>
Expand Down
Loading