Commit Graph

27 Commits

Author SHA1 Message Date
4f7d8df659 Fix trim bar handles invisible: ResizeObserver + rAF
Root cause found via puppeteer inspection:
- trimBarWidth was 4px when renderTrim() ran
- That made calc(32.64% - 12px) = ~-10px, putting handles offscreen left

Modal element gets actual width AFTER appendChild + browser layout pass.
Original code called renderTrim() synchronously right after appendChild,
before the modal had real dimensions.

Fix:
1. Use ResizeObserver on trim-bar to re-render whenever it gets actual width
2. Also use double requestAnimationFrame as fallback (waits for layout)

Verified via puppeteer:
  Before: leftStyle='calc(32.6443% - 12px)' but trimBarWidth=4
  After: handles correctly positioned within visible bar
2026-04-30 11:38:20 +00:00
47d313f39c Debug: log trim modal init values 2026-04-30 11:32:40 +00:00
8d7397699c Trim handles: set initial inline left positions
JS renderTrim() likely failed silently (Cannot read undefined of length).
Set handle positions inline in HTML template so they show immediately
without waiting for renderTrim() to fire.

Added pctOfStr helper to compute percentage as string for inline style.
2026-04-30 11:27:19 +00:00
a3550a444a Fix trim bar handles not visible
Bug from screenshot: trim bar visible but red handles not showing.

Causes:
1. video_duration in job is None for old jobs (was not saved on initial
   processing). Without it, fallback was endInit+60 which placed handles
   off-screen.

2. videoDuration was const, couldn't be updated when video metadata loads.

3. Handle offset was 9px but handles are now 24px wide (need 12px offset).

Fixes:
- Backend /api/transcript: fallback to last segment end time if
  video_duration missing in job
- Frontend: videoDuration is let, updated on loadedmetadata
- Handle offset 9px → 12px for 24px wide handles
- Re-render trim after metadata loads to pick up actual video.duration
2026-04-30 11:21:43 +00:00
446769b68f Trim bar: visible styling (was too transparent on dark theme)
Bug from screenshot: trim bar was invisible due to:
- Background rgba(255,255,255,0.05) too transparent
- Handles 18px width with low contrast
- Removed video controls

Fixed:
- Trim bar background #1a1a1a + 2px #444 border (visible)
- Handles 24px width, full red #ff6b6b with strong glow
- Region 35-20% opacity (brighter)
- Playhead 3px white with shadow (visible)
- Restored video controls
- Added hint text below trim bar
2026-04-30 11:17:57 +00:00
9dba2a1185 iPhone-style trim bar: drag handles instead of sliders
User feedback: 'tako kot imajo na iphonu - potegnem iz leve in iz
desne za na konec... reel pa more biti že v stanju postavljen'

Replaced 2 separate range sliders with iPhone-style trim bar:
- Single horizontal bar showing full video duration
- 2 draggable handles (left = start, right = end)
- Selected region highlighted in accent color
- Live playhead during playback
- Mouse + touch support
- Click anywhere on bar = seek to that position
- Initial state: handles positioned at auto-selected clip range
  (just fine-tune left/right, no need to set from scratch)

formatTime helper for nice m:ss.c display.
2026-04-30 10:34:34 +00:00
7cb4302dcd Edit feature: slider + napis edit + recut endpoint
User insight: 'treba je narediti da ko se reels naredijo da jih lahko
popravljamo... delamo na avtomatiko ampak lahk pa tudi popravljam'

Avto pipeline ostane (Soniox + Claude + render). Po render-u uporabnik
lahko klikne ✏️ Edit gumb in:

1. **Slider za clip start/end**:
   - Vidi 16:9 original video
   - Drag start/end slider z živim preview-om
   - Dolžina prikazana real-time
   - Min 5s, max 60s

2. **Edit napisov** (collapsed, opcijsko):
   - Klik na vrstico → input za popravek besedila
   - Original timestamp ostane, samo besedilo se posodobi
   - Uporabno za 'doline IZBOR' → 'doline IZPOD' tip popravkov

3. **Re-render**:
   - Backend POST /api/jobs/{id}/recut z {start, end, custom_segments}
   - Worker preskoči Soniox + Claude (custom_clip flag)
   - Re-uporabi cached transcript + analysis
   - Re-render samo: clip → reframe → subtitle → output
   - ~30s namesto 3-5 min

New endpoints:
- GET /api/source-video/{id} — 16:9 original za editor preview
- GET /api/transcript/{id} — segmenti + clip range za editor
- POST /api/jobs/{id}/recut — re-render z user timestampi

Worker change: če job ima custom_clip=True, preskoči auto_chorus
analizo in samo re-uporabi obstoječi clip_range iz analysis.json
(updated by recut endpoint).
2026-04-30 10:26:25 +00:00
64b0ed1edc Default 'no subtitles' mode: cleaner reels without text issues
User feedback: subtitles have been causing problems (wrong text from STT,
chorus selection issues). Better to default to clean reels without
burned-in subtitles - just video + audio at the chorus moment.

Changes:
- 'Brez podnapisov' checkbox now CHECKED by default
- Removed 'Stil podnapisov' dropdown from UI (kept hidden for compat)
- Updated step label: 'Reframe v 9:16 + podnapisi' → 'Reframe v 9:16'
- Backend already supports --no-subs flag, no logic changes needed

Result: reels are simpler and more reliable. Just clean 9:16 with audio.
User can enable subtitles per-job by unchecking the box if needed.
2026-04-29 19:38:35 +00:00
108f6b1ad3 Fix: live preview blocks right-side preview buttons
Bug: when user clicked Preview in the left 'Analiza pesmi' panel, it
loaded an inline <video> below it. The video element grew to 400px and,
combined with the sticky positioning of the left card and grid layout,
caused the right column (jobs list) preview/download buttons to become
unclickable — the video element was effectively layering over them.

Fix: replace inline preview with the same modal that the right-side
Preview buttons use. Removes the layout conflict entirely:

- Live panel Preview button now opens the centered modal
- Removed inline #live-video element
- Removed liveVideo references from JS (resetLive)
- Job cards now have data-id and data-title attributes so the modal can
  pull title for display

Both left-side (live) and right-side (jobs list) preview now use the
same clean modal experience.
2026-04-29 15:37:51 +00:00
9df58212b2 Two fixes: clip end overflow into instrumental + UI cleanup
1. CLIP END EXTENSION TOO AGGRESSIVE (Avsenika problem):
   Previous logic extended clip end to any segment within 1s pause.
   This caused clip to spill into instrumental break or next chorus.

   New rules (multi-language):
   - Hard cap: original_clip_end + 3s max (prevents long instrumental tails)
   - Pause threshold tightened: 0.7s (was 1.0s)
   - Length check: skip segments longer than 2.5s (likely new verse/chorus)
   - Outro filler regex: only extend if next segment matches
     (la|na|oh|ah|eh|ej|aj|ja|hey|yeah|yo|ho|wo|hu|mm|nn|uu|oo|aa|ee|ii)
   - Universal across languages (works for SLO 'aj ja ja', EN 'yeah',
     ES 'ay ay ay', RO 'hei hei', JP 'la la la')

2. UI CLEANUP:
   - Removed dead pendingFile/pendingArtist/pendingTitle references
     (multi-upload migration left some single-file resets behind)
   - Job watch handler no longer tries to clear single-file state
2026-04-29 15:14:42 +00:00
91cc03658d Multi-upload batch queue + Telegram notifications
Changes:

1. Frontend multi-upload:
   - File input now has 'multiple' attribute, drag-drop accepts multiple
   - File queue list with per-file artist/title preview + remove button
   - 'Pošlji vse' uploads sequentially (one at a time to avoid network saturation)
   - Each file gets same batch_id for Telegram batch summary
   - After upload, queue clears, jobs appear in right sidebar

2. Backend queue worker:
   - New _queue_worker() background thread processes 'queued' jobs sequentially
   - Only 1 job at a time to keep openclaw stable (avoid CPU/RAM thrash)
   - FIFO order by created_at
   - Auto-starts on app startup after job resume

3. Job submission flow change:
   - /api/process and /api/youtube no longer call background.add_task directly
   - Just mark status='queued', queue worker picks up
   - This means upload completes fast, processing happens in background
   - User can close browser, jobs continue

4. Telegram notifications (FOLX Alerts bot):
   - Per-job: 'Reel pripravljen: Lady Gaga - Abracadabra (29s, 30 MB)'
   - Per-job failed: 'Reel ni uspel: <name> + error message'
   - Batch summary: 'Batch končan: 10/10 reels pripravljeni' (only if >1 in batch)
   - Uses existing TELEGRAM_TOKEN + TELEGRAM_CHAT_ID env vars
   - app/telegram.py module with notify_job_done(), notify_job_failed(),
     notify_batch_complete()

5. batch_id field:
   - Added to Job model + StartJobIn pydantic
   - Saved during upload + process
   - Used to count batch progress and trigger summary notification

User experience:
- Drag 20 videos at once
- Click 'Pošlji'
- Close browser, go grab coffee
- Telegram sends 'Reel pripravljen' for each
- After all done: 'Batch končan: 20/20 reels pripravljeni' summary
- Open app to download all
2026-04-29 15:12:38 +00:00
157e6b781e Fix 'Žena' word still cut: word-level start extension instead of segment-level
Previous fix used segment boundaries — required segments <3s for type 1
or <4s for type 2. But Žena was in a 4.3s segment ('saj še doma mi več
noč'jo verjet'. Žena me'), so the condition wasn't met and clip start
stayed at 77.7s, exactly at end of word 'Žena' (76.88-77.70s).

New approach: scan word-level timestamps directly:

1. If clip start falls MID-WORD → extend back to word start - 0.15s
2. If a word ends 0-0.5s BEFORE clip start AND next word is at clip start
   → that word is suspect (may be first word of chorus that Scribe put
   in previous segment), extend back to its start - 0.15s

Word-level timestamps are always available from Scribe (timestamps_granularity=word).
Falls back to segment-level for local Whisper without word timing.

This handles arbitrary segment lengths and is universal — works for any
language where the chorus starts on a word that the STT placed in the
previous segment.
2026-04-29 15:04:18 +00:00
1cc8e8be35 MXF/MPG broadcast format support: handle multichannel audio properly
Problem: MXF and MPG files (TV broadcast formats) often contain:
- Multiple audio streams (4-8 streams for different language tracks)
- Multichannel layouts (5.1, 7.1) instead of stereo
- Default ffmpeg behavior was -c:a aac without channel limit, which
  meant multichannel got transcoded as multichannel AAC, overwriting
  what should have been clean stereo

Solution:

1. get_audio_streams() helper probes all audio streams with ffprobe
   - Returns codec, channels, sample_rate, language, layout for each

2. build_audio_args() picks best stream + downmix:
   - Prefers first 2-channel stereo stream (usually main mix)
   - Falls back to first stream if none are 2-ch
   - Always: -ac 2 (force stereo downmix), -ar 48000, -c:a aac, -b:a 192k
   - Bitrate raised from 128k to 192k for music quality

3. Smart trim path now detects broadcast formats:
   - .mxf, .mpg, .mpeg, .ts, .m2ts, .mts → transcode (not stream copy)
   - Standard MP4/MOV → stream copy (faster, lossless)

4. Pre-conversion step for broadcast files without trim:
   - Even without --start/--duration, MXF/MPG get converted to MP4
   - Same audio handling as trim path

5. Main render adds explicit -map 0✌️0 -map 0🅰️0? -ac 2 to ensure
   only first video and first audio stream get encoded, with stereo

6. ACR recognize also gets -map 0🅰️0 -ac 2 for MXF compatibility

7. UI accepts: video/*,.mxf,.mpg,.mpeg,.ts,.m2ts,.mts

8. Upload limit raised: 2GB → 10GB (MXF files are large)

This means a TV broadcast MXF with [SLO/EN/DE language tracks] now
correctly outputs stereo MP4 with the main language track preserved.
2026-04-29 14:38:48 +00:00
b543057cee ACRCloud auto-recognition: never block uploads, fall back to fingerprinting
Changes:

1. UI: removed blocking prompt() that asked for artist+title on filename
   that didn't match 'Artist - Title' pattern. Upload always proceeds.
   Instead shows yellow warning saying 'server will try to recognize'.

2. Backend: added scripts/acr_recognize.py — extracts 20s audio sample
   from video (at 15s and 60s offsets for robustness), computes ACRCloud
   fingerprint via native binary (3KB payload), sends to identify API.

3. Pipeline: process_job() now runs ACR recognition step before analysis
   IF parsed_artist or parsed_title is missing. Result is saved to job
   metadata and used for download filename + Scribe/Claude filename hint.

4. Credentials: ACR_HOST + ACR_ACCESS_KEY + ACR_SECRET_KEY env vars
   added to Coolify (using existing keys from openclaw fb-agent metka).

5. requirements.txt: added pyacrcloud==1.0.11 for native fingerprinting.

This unblocks future automation/cron upload pipelines — files don't need
to be perfectly named, ACRCloud will identify them automatically.

Fallback chain:
1. Filename parsing (Artist - Title.mp4)
2. ACRCloud audio fingerprint (works even for '12345.mp4', 'IMG_001.mp4')
3. If both fail: download filename uses 'reel_<id>.mp4' (still works)
2026-04-29 14:24:53 +00:00
3877b822ff Smart download filenames: 'Artist - Title - REEL.mp4' + validation
Two improvements:

1. DOWNLOAD FILENAME: instead of 'reel_<job-id>.mp4' (e.g. reel_25e076af7600.mp4),
   downloads now have descriptive names like:
   - 'Lady Gaga - Abracadabra - REEL.mp4'
   - 'Modrijani - S teboj - REEL.mp4'
   - 'Sarah Connor - FICKA - REEL.mp4'

2. PRE-UPLOAD VALIDATION: when filename doesn't follow 'Artist - Title' format,
   browser prompts user for both fields. Without them, upload is blocked.
   This prevents files with names like '12345.mp4' or 'video_final.mp4' from
   being processed without identifying info.

Implementation:
- parse_artist_title() helper handles common formats:
  - 'Artist - Title.mp4' / 'Artist – Title' (em-dash)
  - 'Artist | Title' / 'Artist : Title'
  - Strips noise: '(Official Music Video)', '(Audio)', '(HD)', '[Lyric Video]'
- Client-side parser mirrors backend (validation before upload)
- Backend accepts artist + title form fields (override parsed)
- Job stored with parsed_artist + parsed_title + has_clean_name fields
- YouTube jobs auto-fetch title via yt-dlp --info-only and parse it
- Filename hint to Scribe/Claude uses parsed values (cleaner than raw filename)
- Download endpoint uses build_download_filename() for content-disposition
- Jobs list shows 'Artist — Title' instead of raw filename

Result: downloaded reels are auto-named correctly for Facebook/Instagram
upload, no more renaming files manually.
2026-04-29 14:15:18 +00:00
671b512917 Fix Preview button in jobs sidebar not opening modal
Root cause: inline onclick with JSON.stringify(title) broke when title
contained quotes, special chars, or was empty. The HTML attribute parser
got confused by mismatched quotes, so click handler never fired.

Fix:
- Replaced inline onclick handlers with data-action attributes
- Added single delegated click listener at document level
- Title stored in element dataset (no HTML quoting issues)
- Added escapeHtml() helper for safe rendering of titles/errors

Now clicking Preview in the right sidebar opens the fullscreen modal
correctly, regardless of filename characters.
2026-04-29 13:39:04 +00:00
389c26d012 Modal preview: click Preview opens fullscreen video player
Previously: clicking Preview in jobs list showed a small inline video
within the job card row.

Now: clicking Preview opens a centered fullscreen modal with:
- Large video player (up to 95vw × 85vh) — same experience as bottom
  live-preview but accessible from jobs list
- Auto-play, controls, native HTML5 video player
- Title shown below video for context
- Download button + Close button
- Click outside or ESC key to close
- Backdrop blur for focus

Removes the obsolete inline <video> element that was rendered hidden
in each job card. Body scroll locked while modal open.
2026-04-29 13:21:36 +00:00
05fb0081c6 Fix preview cutoff + sticky left panel
1. Preview endpoint now supports HTTP Range requests (HTTP 206 Partial)
   - HTML5 video player needs Range support to seek/buffer properly
   - Without it, video would cut off after a few seconds
   - Returns chunks of 64KB on demand

2. Left panel (upload form) is now sticky (position: sticky)
   - Stays in view while right panel (jobs list) scrolls
   - On mobile (<800px) reverts to normal flow
2026-04-29 10:24:32 +00:00
ec71c54570 Upgrade to Sonnet 4.6 + add Gemini 3.1 Pro support
- Refactored analyze_with_claude into shared _build_analysis_prompt + _parse_llm_response helpers
- New analyze_with_gemini() using Gemini 3.1 Pro ($2/M in, MMMLU 92.6% — best multilingual)
- Unified analyze_with_llm(provider) dispatcher with auto-fallback (Claude → Gemini)
- API endpoint accepts llm_provider in StartJobIn (claude/gemini/auto)
- Frontend dropdown to pick LLM
- Default model is now Sonnet 4.6 (was Haiku 4.5) — 3x quality at 3x price (~3 cents/video)
- Gemini support is opt-in: needs GEMINI_API_KEY env var to activate
2026-04-29 08:26:27 +00:00
69fb2f5ce8 Upgrade default Whisper model: small/medium → large-v3 for much better Slovenian/Slavic transcription accuracy 2026-04-29 08:20:18 +00:00
4e123bdabc UI: hide lang/model dropdowns — both are fully automatic now (3-sample lang detection + medium default model) 2026-04-29 08:03:22 +00:00
af3c933c78 Robust language detection + anti-hallucination
- 3-sample voting for auto-detect (start/middle/end of song) prevents lang switching mid-song
- Lock detected language for full transcription
- Anti-hallucination: condition_on_previous_text=False, temperature=0.0
- compression_ratio_threshold=2.4 (rejects repetitive hallucinations)
- log_prob_threshold=-1.0 (rejects low-confidence segments)
- no_speech_threshold=0.6 (more aggressive silence detection)
- Default Whisper model changed: small → medium (better for all langs incl. Slavic)
2026-04-29 07:59:20 +00:00
c870d80726 Fix: extend clip if ends mid-vocal (no chorus cut-off), DejaVu Sans font (supports SLO/HR/BS chars), auto-upgrade to medium Whisper model for Slavic languages 2026-04-29 07:35:00 +00:00
8512076b91 Major: smart selection pipeline (analyze.py) + audio fade + multi-lang auto-detect
- New analyze.py: full transcript + energy + structural analysis
- Smart clip range: includes pre-chorus, can exceed 30s up to max_duration (default 45s)
- Audio fade in/out: auto-detected from vocal boundaries
- Instrumental detection: auto-disables subs if vocals < 10% of duration
- Multi-language: auto-detect via Whisper or explicit (DE/SL/HR/BS/SR/EN/IT/ES/FR)
- Frontend: cleaner UX, added bs language, smart selection description
- reframe.py: --fade-in --fade-out args
- clip.py: propagates fade params
- app/main.py: replaces find_chorus.py call with analyze.py
2026-04-29 06:21:35 +00:00
bf7ced5c7b Reset upload form also after failed jobs (so next upload works) 2026-04-28 16:29:39 +00:00
c34e4aa376 UX: Live progress panel below upload form, stable progress bar, inline preview/download 2026-04-28 16:19:40 +00:00
30b969e4b8 Initial: reels clipper app
- FastAPI backend (auth, jobs, SSE, download)
- Frontend: drag&drop + YouTube URL + jobs panel
- Pipeline: yt_download → find_chorus → reframe → subtitle
- Modes: track (face follow), center, blur
- Whisper for SI/DE/EN subtitles
- Auto-chorus detection via Whisper + RMS energy
- Docker + Coolify ready
2026-04-28 15:28:22 +00:00