Commit Graph

58 Commits

Author SHA1 Message Date
Claude
d2c9a48cc2 Google Sign-In auth + email whitelist + session cookies
WHY: Doslej basic auth z deljenim AUTH_USER/AUTH_PASS — tvegan za delitev
z drugimi (urednika ipd.). Z Google Sign-In dobi vsak uporabnik svoj
account, dostop pa nadzira whitelist v env.

NEW BACKEND:
- GET  /login              — login stran z Google Sign-In gumbom (GSI popup)
- POST /auth/google/callback  — verify Google ID token + set session cookie
- GET  /auth/me            — vrne email + auth method (za frontend header)
- GET  /logout             — pobriše cookie + redirect /login
- POST /logout

AUTH FLOW:
1. Browser GET /              → check_auth() → ni session → redirect /login
2. /login                     → Google Sign-In popup (preko gsi/client.js)
3. User izbere Google account → JS pošlje credential na /auth/google/callback
4. Server verifies token (oauth2.googleapis.com/tokeninfo)
5. Email mora biti v ALLOWED_EMAILS env
6. Set HttpOnly+Secure session cookie (HMAC-SHA256, 30 dni)
7. Redirect /

SECURITY:
- Session token: base64url(email|expiry|HMAC). Server ne ranji ne hrani.
- HMAC z SESSION_SECRET (auto-derived iz AUTH_USER+AUTH_PASS če ni nastavljen)
- Cookie HttpOnly + Secure + SameSite=lax
- Token verify: aud check, iss check, exp check, email_verified check

BACKWARD COMPAT:
- HTTPBasic auth še vedno deluje (cron, scripte, API klici)
- check_auth() probaj prvo cookie, potem basic
- Brez GOOGLE_CLIENT_ID env: vse še vedno dela na basic auth

ENV VARS (treba dodati v Coolify):
- GOOGLE_CLIENT_ID=938379241163-pvb328plec2207rbtufic8u5fgb6mkn9.apps.googleusercontent.com
- ALLOWED_EMAILS=sebastjan.artic@gmail.com,ales.cadez@gmail.com
- SESSION_SECRET=<random 64-char hex> (optional — defaultni se izračuna)

FRONTEND:
- Header: 👤 email + ↪ Odjava gumb (samo ko je prijavljen)
- DOMContentLoaded fetcha /auth/me, prikaže email
2026-05-04 12:26:53 +00:00
Claude
12e8edba93 Delete job: cascade delete povsod (Nextcloud + dedup DB + S3 + lokal)
PROBLEM: '✕' gumb je do zdaj brisal samo lokalne fajle + S3, ampak
NE Nextcloud upload (rel je ostal na folxspeed/REELS/{TV}/) in NE
dedup DB zapis (zato se enaka pesem ni mogla več upload-ati).

NEW BACKEND:
- _nextcloud_delete(filename, target_subdir) helper preko WebDAV DELETE
  (404 šteje kot success — če že ne obstaja, OK)
- delete_job() razširjen:
  1. Nextcloud delete (če nextcloud_status='uploaded' ali ima nextcloud_url)
  2. Dedup DB remove (processed_videos zapis za tisto TV postajo)
  3. Lokal + S3 delete vseh workfile-ov (kot prej)
  4. Glob za yt-dlp artifacte ({job_id}_yt*) — info.json, .part, .f137.mp4
  5. Job metadata
- Response: {deleted, nextcloud_delete: 'ok'|'not_found'|'fail: msg', nextcloud_filename}

NEW FRONTEND:
- buildJobEl() doda data-nc-status atribut na kartico
- deleteJob() bere dataset.ncStatus za Nextcloud info
- Confirm dialog detail razložen seznam KAJ se zbriše + opozorilo če Nextcloud
- Če Nextcloud delete ni uspel po API klicu, alert
2026-05-03 14:52:40 +00:00
Claude
79f611ba73 Retranscribe feature: ponovi STT z drugim providerjem v Edit modalu
PROBLEM: STT (Soniox/Scribe) včasih popolnoma narobe slišije besedilo —
npr. 'BILA JE LJUBEZEN PRVA' postane 'BILAL JO ME ZANPRLA' (TRIO ŠUBIC).
Edit modal je do zdaj zahteval popoln recut z novim clip range.

NEW: '🔁 Ponovi transkript' gumb v Edit modalu z dropdown za provider:
  - Scribe (ElevenLabs) — default, najboljši za nemščino + slovenščino
  - Soniox — slovenski default v auto routing
  - Whisper local (faster-whisper)

Backend:
  POST /api/jobs/{id}/retranscribe { provider, auto_upload?, lang? }
  - Briše stari MP4 + analysis.json + .srt + .ass (lokal + S3)
  - Re-queue job z whisper_provider override
  - Ohrani isti clip range — analyze.py si naredi svežo analizo
  - retranscribe_count števec za sledenje poskusov

UX: confirm dialog → 1.5s status → auto-close modal → watchJob za live progress.
Brez auto_upload v Nextcloud — preveri rezultat preden re-uploadaš.
2026-05-03 14:38:35 +00:00
Claude
576cc807b5 Fix parse_artist_title (ANS.* bug) + unify station naming na FOLX SLO
PROBLEMS:
1. parse_artist_title je uporabljal Path(s).stem za stripping ext-a,
   kar je pri YT title 'ANS.NAVEZA - SREČA OPOTEČA' vrnilo 'ANS' (Path
   smatra '.NAVEZA - SREČA OPOTEČA' kot extension). Posledica: parser
   failed → ACR fallback → 'Folx' kot artist na 15+ jobih.

2. tv_station je imel dva imena: 'FOLX SLOVENIJA' (frontend default)
   in 'FOLX SLO' (qnet match output) — UI tabi niso seštevali pravilno.

FIXES:
- parse_artist_title: ext stripping samo za znane ekstenzije
  (.mp4, .mp3, .m4a, .webm, .mkv, .avi, .mov, .wav, .flac, .aac,
   .opus, .ogg, .wmv, .mxf), NE za naključne pike v YT title.
- Vsi defaultni 'FOLX SLOVENIJA' → 'FOLX SLO' v Pydantic modelih +
  templates + filter tabi.
- Nextcloud mapping STATION_TO_NEXTCLOUD_FOLDER ostane nespremenjen
  (FOLX SLO → mapa 'FOLX SLOVENIJA', kjer pač zaplane).

BACKFILL (že apliciran prek scripte):
- 15 jobov z parsed_artist='Folx' popravljenih na pravi izvajalec
  iz youtube_title (ANS.NAVEZA, ANS.BITENC, ANS. ROKA ŽLINDRE, ipd.).
- 86 jobov tv_station 'FOLX SLOVENIJA' → 'FOLX SLO'.
2026-05-03 14:26:42 +00:00
Claude
0d72d70f5d S3 mirror integration: workfiles auto-mirror to s3://folxspeed/reels-app/
- main.py: 4 helper funcs (_persist_to_s3, _ensure_local, _delete_from_s3,
  _ffmpeg_then_persist) - no-op fallback when S3 creds missing
- save_job(): mirror metadata JSON to S3
- process_job(): mirror YT download + render output + analysis/srt/ass to S3
- upload_video(): mirror direct uploads to S3
- _precache_edit_assets(): Popen->threaded with S3 sync after ffmpeg
- read endpoints (download, preview, source_video, waveform, preview_clip,
  get_transcript, recut_job): _ensure_local() fallback fetch from S3
- delete_job(): cascade delete to S3 (mirror unlink)
- cleanup.py: NEW module, deletes local files >48h that exist in S3.
  Verified by S3 head_object + size match. NOT YET ACTIVATED in cron.

Backward compat: lokalna mapa ostane primary. Brez env vars S3_* vsi
helperji vrnejo False (no-op). Production behavior identičen, dokler
ne dobi S3 creds.
2026-05-03 12:24:18 +00:00
Claude
ec1d109e3b S3 storage module: boto3 abstraction for reels-app workfiles (uploads/outputs/jobs prefixes) 2026-05-03 11:57:12 +00:00
OpenClaw Agent
6a9e20da19 Nextcloud upload: mapping Qnet station -> NC folder. Qnet baza ima FOLX SLO/ONE/ZWEI, Nextcloud pa FOLX SLOVENIJA/ONE DE/ZWEI MUSIC \u2014 STATION_TO_NEXTCLOUD_FOLDER dict normalizira pri uploadu. Re\u0161i HTTP 404 NotFound za FOLX SLO jobe (106 jobov). 2026-05-03 12:24:35 +02:00
OpenClaw Agent
968eba7205 YT metadata fetch: razširi --info-only output (id, uploader, description, upload_date, view_count, tags, ...). Single video submit fetcha metadata + Qnet match takoj (kot playlist). Worker preskoči info fetch če metadata že obstaja, sicer shrani vsa polja in naredi Qnet match.
- yt_download.py: get_info() probaj najprej yt.biba.live API /download/info (residential IP, sveži cookies), fallback na lokalni yt-dlp. --info-only output razširjen na 17 polj.
- main.py submit_youtube single video: fetcha metadata (yt_get_info) ob submit, shrani youtube_title/uploader/id/description/duration/thumbnail/upload_date in naredi Qnet match (parity s playlist branch).
- main.py worker: skip info fetch če youtube_title in youtube_uploader že obstajata. Sicer shrani VSE polja + Qnet match + parser fallback.
2026-05-02 15:54:28 +00:00
OpenClaw Agent
77075795ce YouTube playlist support: /api/youtube/playlist-preview za pred-confirm, /api/youtube zaznava 'list=' URL in kreira batch (1 job/video). Qnet auto-match na YT naslovu, Confirm dialog v UI z prvih 5 naslovov. 2026-05-02 12:34:27 +00:00
OpenClaw Agent
7a7d7ea20d Preview cache-busting: Cache-Control: no-cache na endpoint + ?v=Date.now() v frontend URL — browser ne sme cachat starega output-a po recutu 2026-05-02 12:24:19 +00:00
OpenClaw Agent
6270c92b44 STT routing: FOLX DE / ZWEI \u2192 Scribe default (4\u00d7 hitrej\u0161e + brez Mississippi/Mrs. Sadie halucinacij). SLO postaje ostanejo na Soniox. User lahko override v UI. 2026-05-02 11:17:51 +00:00
OpenClaw Agent
1f8565413a Qnet match VEDNO shranjen v job.qnet_match (audit + tv_station auto-fill), tudi ko client po\u0161lje artist+title 2026-05-02 10:57:09 +00:00
OpenClaw Agent
24e1b53aa8 Qnet match v upload queue — auto-prepoznavanje pesmi takoj ko izbere\u0161 fajle (parallel POST /api/qnet/match-batch). \u010ce baza prepozna komad, prikaz Artist \u2014 Title z station badge namesto 'Brez razvidnega imena'. 2026-05-02 10:47:51 +00:00
OpenClaw Agent
b938d1e4d8 Qnet song match — fetcha Songs.txt iz 5 MB playerjev (FOLX DE/SLO, ZWEI, ONE, ADRIA), 20K+ songs, fuzzy match na upload-u → clean parsed_artist/parsed_title + auto tv_station. /api/qnet/{stats,match,sync} 2026-05-02 10:42:35 +00:00
4febf0b844 Trailing bare 4K/HD/8K (brez oklepajev)
Edge case: 'Naslov (Official video) 4K.mxf'
Pred: '(Official video)' odstranjen, ampak '4K' brez oklepajev ostane

Pattern: r'\b(?:4K|HD|HQ|8K|1080p|720p)\s*$'
- Samo na koncu stringa
- Ne v sredini (Top 100 hitov ohranjen)
2026-05-02 08:55:44 +00:00
2aec7f7a29 Odstrani trailing 2-4 cifrene številke (leto/verzija)
User: 'včasih številka 23 pri Modrijanih in pri Firbcih, te se pojavlajo
v naslovih'

Razlog: NAS interno označeni kot leto produkcije.
'PA KAJ (Official Video) 23' = leto 2023
'ONA HOČE (Official video) 22' = leto 2022
'S tabo res rad (Official Video) 33' = leto 2033 (?) ali interno

Pattern: r'\s+\d{2,4}\s*$' — trailing 2-4 cifrena številka

Test rezultati:
- 'S tabo res rad 33' → 'S tabo res rad' 
- 'PA KAJ 23' → 'PA KAJ' 
- 'PESEM GORENJSKIH TRAT (2023)' → 'PESEM GORENJSKIH TRAT' 

Edge cases (NI odstranjeno):
- 'Pesem 25 ljubljanskih ulic' (število v sredini) 
- 'Top 100 hitov' 
2026-05-02 08:52:40 +00:00
4e2c690bc5 Bolj agresivno čiščenje filename: () prazni + catch-all noise besede
User: 'Topliška pomlad — KAR PADA NAJ SNEG ( - ) — tile oklepaji
pa Official video itd. Daj ko se nalaga na Nextcloud mora biti samo
izvajalec in naslov komada.'

Dodatni NOISE_PATTERNS:
1. Prazni / dummy oklepaji: '( )', '( - )', '(-)', '(.)' itd.
2. Catch-all za oklepaje z noise besedami:
   video|audio|version|mix|edit|remix|cover|live|hd|hq|4k|8k|
   remaster|extended|clean|explicit|radio|lyric|official|musik
3. Avtor/producer brackets: '(prod. by X)', '(feat. Y)', '(ft. Z)'

Test rezultat:
'Topliška pomlad - KAR PADA NAJ SNEG ( - )(Official 4K Video).mp4'
→ 'Topliška pomlad - KAR PADA NAJ SNEG - REEL.mp4'

'Sarah Connor - FICKA (Offizielles Musikvideo).mp4'
→ 'Sarah Connor - FICKA - REEL.mp4'

Vsi novi uploadi bodo imeli čista imena.
TODO ločeno: rename obstoječih 31 datotek na Nextcloudu (skript pripravljen)
2026-05-02 08:10:30 +00:00
376bb4db09 Manual Nextcloud upload tudi nastavi hidden_after_upload=True
User: 'naredi da ko damo IN sejvamo na nextcloud da izgine iz pregleda
in se vrne če stisnemo Pokaži že naložene'

Bug: 2 mesti uploadamo na Nextcloud:
1. Avto-upload po recut/Save → hidden_after_upload=True 
2. Manual ☁ Nextcloud klik → samo nextcloud_status='uploaded' 

Fix: oba puta nastavi hidden_after_upload=True

Tudi: batch fix obstoječih 19 uploaded jobs ki niso imeli hidden flag,
posredno preko docker exec — zdaj se skrijejo tudi oni.

Workflow zdaj:
- ☁ Nextcloud klik → upload + hidden=true → izgine
- Save (Edit) → re-render + auto-upload + hidden=true → izgine
- ☐→☑ 'Pokaži tudi že naložene' → vidiš vse z zelenim borderjem
2026-05-02 08:01:37 +00:00
bdc1d14498 Toggle 'Pokaži tudi že naložene' + recut prepiše Nextcloud
User feedback: 'načeloma bi se moral samo če kaj spregledamo in že
sejvamo pa ne moremo nazaj vrniti in popraviti'

Frontend:
- Nov toggle '☁ Pokaži tudi že naložene' nad 'moji reels' headerjem
- Default OFF: vidiš samo aktivne (ki niso uploaded)
- ON: vidiš VSE, vključno z uploaded (z zelenim borderjem)
- buildJobEl: uploaded reels imajo border-left zelen + bg #4ade80 0.04 opacity
- Listener: change event sproži refreshJobs()

Backend:
- recut endpoint: reset hidden_after_upload=false, nextcloud_status='recutting'
- Po končanem recut: avto-upload na Nextcloud (PUT prepiše obstoječi file)
- Hidden_after_upload=true spet po uspešnem re-uploadu

Workflow:
1. Reel uploaded → hidden, ni v UI
2. Klik toggle 'Pokaži tudi že naložene' → vidiš ga (zelen border)
3. Edit → Save → re-render (visible spet, status='processing')
4. Re-upload PUT na Nextcloud → prepiše obstoječi file (ista pot, isto ime)
5. Hidden=true → izgine spet (osim če toggle on)
2026-05-02 07:23:03 +00:00
f2034f9970 Dedup: SQLite baza za že obdelane komade
User feedback: 'dodaj da če čekira in shranjuje že obdelani komadi v SQL bazo,
da če nalagamo komad ki smo ga že naložili da ga ne naloži'

NEW: SQLite dedup database at /data/processed.db
Schema: processed_videos
  - normalized_name (PK part 1)
  - tv_station (PK part 2) — isti komad lahko obstaja na različnih postajah
  - filename_orig
  - job_id
  - nextcloud_url
  - file_size_mb
  - uploaded_at

Filename normalization removes noise:
  'BRAJDE (Official Video).mp4' → 'brajde'
  'Brajde (HD).mxf' → 'brajde'
  'BRAJDE - LIVE 2024.mp4' → 'brajde'
(strips parentheses, suffixes like Official/HD/4K/Live, extension, lowercase)

NEW endpoints:
- POST /api/dedup/check — preveri katera imena so že obdelana
- POST /api/dedup/remove — pobriše dedup zapis (Re-process)
- GET /api/dedup/list — seznam vseh obdelanih (opt. filter po tv_station)

Integration:
- Nextcloud upload (manual + auto): zabeleži v dedup po uspešnem PUT
- File queue (frontend): pred dodajanjem preveri dedup
  → prikaže rdeč warning '⚠ Že naložen na ONE DE (29.4.2026) — Re-process'
  → opacity 0.6 (vizualno blediji)
  → submit jih SKIP-a (osim če 'Re-process' kliknil)
2026-04-30 15:00:10 +00:00
16c332b490 Save → avto-upload v Nextcloud → reel izgine iz seznama
User feedback: 'zaenkrat bomo ročno popravljali in pregledovali. Ko kliknemo
Save potem se shrani v pravi folder in izgine.'

Workflow:
1. User izbere TV postajo (zavihek)
2. Naloži komade
3. Reel se renderira (auto chorus)
4. User pregleda + Edit če treba
5. Save → re-render z user popravki
6. Po končanem re-render: AVTO-upload v Nextcloud /folxspeed/REELS/{station}/
7. Reel IZGINE iz seznama (hidden_after_upload flag)

Backend changes:
- RecutRequest: nov field auto_upload (default True)
- update_job: shrani auto_upload_to_nextcloud
- process_job done block: če flag set + Nextcloud configured →
  upload + nextcloud_status='uploaded' + hidden_after_upload=True

Frontend changes:
- refreshJobs: filter out jobs with hidden_after_upload
- TV station badge na vsaki kartici (z emoji + ime postaje)
- Vidiš na prvi pogled kam bo šlo

Workflow rezultat: po Save reel izgine, je avtomatsko v pravi mapi
2026-04-30 14:53:01 +00:00
1c11dfe630 TV station tabs + per-station Nextcloud upload target
User feedback: 'Sarah Connor in Abracadabra grejo v ONE DE, ne v FOLX SLO.
Naredi zavihke za vsako TV postajo (FOLX SLO, FOLX DE, ONE DE, ZWEI MUSIC, ADRIA)
in upload gre v ustrezno Nextcloud podmapo.'

Backend changes:
- StartJobIn + YouTubeJobIn: nov field 'tv_station' (default 'FOLX SLOVENIJA')
- update_job: shrani tv_station v job JSON
- POST /api/jobs/{id}/upload-nextcloud:
  bere tv_station iz job, target_subdir = folxspeed/REELS/{station}

Frontend changes:
- 5 TV station tabs: FOLX SLOVENIJA (active), FOLX DE, ONE DE, ZWEI MUSIC, ADRIA
- Hidden input #tv-station-input drži current selection
- Klik na tab ga aktivira (accent color)
- collectSettings() vključuje tv_station

Manual fix: Sarah Connor in Abracadabra job.json popravljena → tv_station=ONE DE
Reset njihovih nextcloud_* polj da bo upload v pravo mapo.
2026-04-30 14:38:45 +00:00
8284181fb3 Cleanup: odstrani duplikat upload-nextcloud endpoint
Endpoint /api/jobs/{id}/upload-nextcloud je že obstajal (commit dbb8ab3)
in deluje. Moja nova varianta je bila duplikat — odstranjena.

Aktivni endpoint: line 1593 (upload_nextcloud), uporablja
_nextcloud_upload() in _nextcloud_configured() helper-je.

Zdaj imamo:
- _safe_filename_for_nextcloud() helper (ostane, lahko pride prav)
- Frontend gumb '☁ Nextcloud' z 4 stanji (default/uploading/uploaded/failed)
- 14 reelov že uspešno uploadanih v /folxspeed/REELS/FOLX SLOVENIJA/
2026-04-30 14:32:04 +00:00
d03beddd0d Nextcloud: URL-encode path segments + use FOLX SLOVENIJA subfolder
- NEXTCLOUD_FOLDER env updated: folxspeed/REELS → folxspeed/REELS/FOLX SLOVENIJA
- urllib.parse.quote() each segment (handles spaces in folder names)
- e.g. 'FOLX SLOVENIJA' → 'FOLX%20SLOVENIJA' in URL
2026-04-30 14:27:28 +00:00
dbb8ab3059 Nextcloud upload za FOLX SLOVENIJA reels
User wants reels saved directly to Nextcloud /folxspeed/REELS/FOLX SLOVENIJA/

NEW backend endpoint: POST /api/jobs/{id}/upload-nextcloud
- WebDAV PUT preko stdlib urllib (no new deps)
- Uses NEXTCLOUD_URL/USER/PASS/REELS_PATH env vars
- Updates job status: uploading → uploaded / error
- Stores nextcloud_url + nextcloud_error in job

Frontend already had button (☁ Nextcloud) and handler — just needed
backend endpoint. UI states:
- ☁ Nextcloud (blue) — not yet uploaded
- ☁ ✓ Nextcloud (green) — uploaded successfully
- ☁ ✕ Poskusi znova (red) — upload failed (hover for error)

Env vars added in Coolify:
- NEXTCLOUD_URL=https://nextcloud.folx.tv
- NEXTCLOUD_USER=admin
- NEXTCLOUD_PASS=<app token>
- NEXTCLOUD_REELS_PATH=folxspeed/REELS/FOLX SLOVENIJA
2026-04-30 14:23:15 +00:00
1d6af29a23 Parallel workers (3) + pre-cache Edit assets
User feedback: 'ne morem nič drugega delat dokler izvaža reel?
a če bi bile večje mašine bi blo bolj?'

Without GPU upgrade, optimize CPU usage:

1. PARALLEL WORKERS:
   - Was: 1 worker thread, processes 1 job at a time
   - Now: NUM_WORKERS=3 parallel threads (configurable via env)
   - Each worker locks its job atomically (set instead of single var)
   - 3 reels render simultaneously instead of sequentially
   - Edit feature usable while other reels render

2. PRE-CACHE EDIT ASSETS:
   - On job done, fire-and-forget ffmpeg subprocess.Popen for:
     * low-q source video (480p) — used in Edit modal video player
     * waveform PNG (2400x72) — used in Edit modal trim bar
   - Both run in background, don't block pipeline
   - When user later clicks Edit, assets already cached → modal instant
   - On-demand fallback still works if precache failed

Result: Edit modal opens instantly even while other reels render.
3 reels can render in parallel = ~3x throughput on multi-core CPU.
2026-04-30 12:55:38 +00:00
facfd6bd39 Edit modal: waveform + napisi desno + live highlight
User feedback: 'če bi imeli spodaj wave strukturo bi se po tem prmikali,
in narediti da teksti laufajo na desni strani ob robu videja'

NEW: backend /api/waveform/{id}?width&height
- ffmpeg showwavespic generates PNG (~10-50KB)
- Cached forever per song
- Red color (#ff6b6b) matching accent

Frontend layout RESTRUCTURED:
- 1200px max-width (was 900px)
- Top section: GRID 1fr / 320px
  - LEFT: video (16:9)
  - RIGHT: napisi panel (sticky header, scrollable, 55vh max)
- Bottom: trim bar full-width with WAVEFORM as background image
- Hint text updated: 'Klik na valove ali napise = skoči video'

INTERACTIONS:
- Click segment row → seekToSegment() jumps video to that timestamp
- Live highlight: gold (#ffd700) on currently playing segment
- Auto-scroll panel to keep active segment in view
- Drag handles updates segment row colors (in-clip = red bg, outside = gray)
- Click on trim bar (waveform) still works as seek

User can now:
- See visual audio shape (loud parts = vocals, quiet = instrumental)
- See ALL napisi at once on the right
- Click any napis to jump to it
- Watch live highlight follow the song
- Edit any napis text inline
2026-04-30 12:25:08 +00:00
c94e6214ca Edit: instant client-side preview with low-q source
User feedback: 'predvaja odsek in začne iz nule kar ni ok, ne moremo
premikati levo dolj levo... za to bi rabili low-q?'

REPLACED render-on-demand approach with low-q source download:

1. Backend: GET /api/source-video/{id}?quality=low
   - 480p re-encode of full source (cached after first request)
   - veryfast preset, CRF 28
   - First request: ~5-10s (depends on song length)
   - Subsequent: instant (cached)

2. Frontend: Edit modal loads ?quality=low
   - 'Pripravljam predogled (~5s prvič, potem instant)' status
   - Once loaded: ALL preview is client-side instant
   - 'Predvajaj odsek' jumps to trimStart and plays
   - Auto-stop at trimEnd (loops back)
   - Drag handles DURING playback = instant seek (browser scrubs in 5MB)
   - Drag NOT blocked during play (you can fine-tune in/out live)

3. Removed old /api/preview-clip endpoint logic (no longer needed)
   Note: kept the route as cache cleanup for old jobs

Workflow now:
- Open Edit → 5s wait first time
- Drag handles freely (instant scrubbing)
- Click Predvajaj → starts at trimStart immediately
- Drag handles WHILE playing → live preview
- Save when satisfied → 70s full render
2026-04-30 12:14:37 +00:00
63da3ad2e2 Preview-clip: validate cache, support HEAD, cleanup on fail
Bugs from puppeteer inspection:
1. Old buggy renders left 0-byte cache files behind. New code never
   re-rendered because cache_path.exists() was True.
   Fix: validate cache file is >1KB, otherwise re-render.

2. FastAPI @app.get only handles GET, not HEAD. Frontend's HEAD check
   returned 405, then GET re-rendered (correct), but second click also
   returned 405 then 200 again — confusing.
   Fix: use @app.api_route with methods=['GET', 'HEAD']

3. If ffmpeg fails partway, broken file remains in cache.
   Fix: unlink on any failure path.

Also deleted existing empty cache files in container.
2026-04-30 12:05:11 +00:00
69062205fd Fix preview-clip ffmpeg: force even dimensions for libx264
Bug: 'width not divisible by 2 (853x480)' from screenshot.
libx264 requires even width/height. scale=854:480 + decrease can result
in 853x480 (odd width).

Fix: chain second scale filter that truncates to nearest even number:
  scale=trunc(iw/2)*2:trunc(ih/2)*2

Verified locally: 4.4MB clip in 4.8s on CPU.
2026-04-30 12:01:06 +00:00
0513768466 Live preview in Edit modal — fast low-q clip render
User feedback: 'dejstvo je da trajna ker more najprej zrenderirat? to traja?
za to bi rabili hudo mašino al?'

Solution before GPU upgrade: live preview that renders just the selected
range as low-quality 480p clip. ~2-3s instead of ~70s full reel render.

NEW endpoint: GET /api/preview-clip/{job_id}?start=X&end=Y
- ffmpeg fast extract (no reframe, no subtitles, no face tracking)
- 480p ultrafast x264 preset, CRF 30
- Cached per job+range (re-clicks are instant)
- ~2-3s on CPU

Frontend:
- '▶ Predvajaj odsek' button now triggers preview-clip render
- Shows status: '🎬 Renderiram odsek... (~3s)'
- After render: video element switches to preview src
- User sees EXACTLY what reel will contain (just without face track)
- Subsequent clicks on same range are instant (cached)

Workflow:
- Drag handles → click '▶ Predvajaj odsek' → 3s wait → see + hear it
- Iterate fast: drag → preview → drag → preview
- Final ' Shrani in re-render' only when satisfied (~70s full render)
2026-04-30 11:56:54 +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
b4294e7113 Fix typo: OUTPUTS_DIR -> OUTPUT_DIR (causing 500 in /api/transcript) 2026-04-30 10:30:18 +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
dc1cb1ad27 Fix SRT subtitle timing: use word-level timestamps for chunk boundaries
Bug: BRAJDE reel showed subtitles 2-3 seconds out of sync with audio.

Soniox returned correct word timestamps:
- 'Ajmo,' at 41.82s
- 'Janezi!' at 42.18s
- 'Pejd' greva, ajde,' at 43.44-44.40s

But generate_srt_from_segments() ignored word timestamps and split long
segments into evenly-spaced 2.5s chunks based on segment duration:

  chunk_dur = duration / n_parts   ← assumes even pacing
  for i in range(n_parts):
      cs = rel_start + i * chunk_dur

This produces wrong timing because singers don't sing evenly. Real audio
had 'Ajmo, Janezi!' in 0.9s and 'Pejd' greva, ajde, na traktorju od Majde'
in 6s — the 2.5s chunks didn't align with vocals.

Fix: when word-level timestamps are available (Soniox/Scribe), group
words into chunks where each chunk's start/end match the actual first/last
word timestamps. Each chunk is at most MAX_CHUNK_DURATION (2.5s) but
respects natural word boundaries.

Before:
  00:00.000 → 01.900  AJMO, JANEZI! PEJD' GREVA, AJDE, NA TRAKTORJU OD
  00:01.900 → 03.800  MAJDE, NOBEN NAJU NE NAJDE, KO PELJEM TE

After:
  00:00.020 → 02.120  AJMO, JANEZI! PEJD' GREVA,
  00:02.360 → 04.820  AJDE, NA TRAKTORJU OD MAJDE, NOBEN

Subtitles now perfectly align with vocals.
2026-04-30 04:02:09 +00:00
4788a55643 Clean noise patterns more aggressively + clean already-stored values
User feedback: 'Ansambel UNIKAT — PA PA (offiicial video)' shows the
'(offiicial video)' suffix everywhere (titles, downloads, UI). The user
wants only 'Artist - Title' without any video format markers.

Two fixes:

1. EXPANDED _NOISE_PATTERNS to handle:
   - Typos in 'official': 'offiicial', 'offical', 'oficial' (regex Off[a-z]*icial)
   - Variants: '(Official 4K Video)', '(Official HD Video)', '(Official Music Video)'
   - More versions: (Live), (Cover), (Acoustic), (Extended Mix), (Radio Edit), (Clean), (Explicit)
   - Square brackets: [Official...], [HD], [Lyrics...]
   - Bare words without brackets
   - Trailing year markers '(2024)'

2. NEW clean_noise() function applied at READ TIME:
   Even if a job was saved with 'PA PA (offiicial video)' as parsed_title,
   the new code re-cleans it when serving the job to the UI or building
   the download filename. This means existing jobs get fixed too without
   needing re-processing.

3. Applied to:
   - build_download_filename() — clean before formatting
   - list_jobs() — strip noise when serving job list
   - get_job() — strip noise when serving single job

Result: 'Ansambel UNIKAT - PA PA - REEL.mp4' (no more (offiicial video))
2026-04-29 18:47:19 +00:00
df6011c3cf Detect Scribe hallucinations + filter from SRT + auto-retry
Bug found in Žena ME TEPE third re-test:
- Scribe transcribed only verse 1 (0-33s) properly
- Then returned a single 98s segment [34.7-133.2] with just 1 word 'sam'
- This is a known Scribe hallucination on instrumental sections
- Result: SRT showed 'SAM SAM SAM SAM...' 14 times across the chorus
- Looked completely wrong because the chorus audio was correct but
  subtitles showed 'SAM' repeatedly

Three-part fix:

1. SRT GENERATOR: skip segments > 15s with < 5 words.
   These are hallucinations and have no real transcription value.

2. SCRIBE TRANSCRIBE: detect hallucinations in returned segments.
   - Mark segments > 15s with < 5 words as hallucinations
   - Compute true coverage % (excluding hallucinations)
   - Add _hallucination_count and _coverage_pct to result

3. TRANSCRIBE_FULL: auto-retry Scribe if quality is poor.
   - If hallucinations detected OR coverage < 50%, retry once
   - Keep retry result only if it has better stats
   - Otherwise fall back to first attempt (still better than nothing)

This makes the pipeline robust against Scribe's occasional bad transcripts
on songs with long instrumental breaks. Most second attempts succeed
where the first failed (random Scribe variance).
2026-04-29 18:08:35 +00:00
d73453fe50 Fix SRT subtitles: word-level clipping for partial segments
Bug found in Žena ME TEPE re-test:
- Clip start: 76.73s (correct, captures full 'Žena' word)
- But SRT subtitle #1 showed: 'SAJ ŠE DOMA MI VEČ NOČJO VERJET.'
- That text is from the PREVIOUS verse, not the chorus!

Why: previous segment (73.9-78.2s) contained 'saj še doma mi več
nočjo verjet. Žena me'. Clip start fell at 76.73s (mid-segment).
Old SRT logic: max(s_start, clip_start) just clipped TIMING but kept
ALL the text from that segment, including text from before the clip.

Fix: when a segment partially falls outside clip range AND has word-level
timestamps (Scribe provides these), reconstruct the segment using only
the words that actually fall within [clip_start, clip_end]. Audio
(clipped at clip_start) only contains those words anyway, so the
subtitle should match.

Result for Žena chorus:
- Old: 'SAJ ŠE DOMA MI VEČ NOČJO VERJET.' (wrong, that text is silent
        in clip)
- New: 'ŽENA ME' (only words actually heard at 76.73-78.16s)
2026-04-29 16:48:39 +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
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
68247bb84c Integrate ElevenLabs Scribe (best multilingual STT 2026)
ElevenLabs Scribe replaces local Whisper as default transcription:
- 96.7% accuracy English, 2.4% WER Indonesian (vs Whisper 7.7%)
- 18x faster (200s song = 11s vs 3-5 min on CPU)
- No hallucinations on songs (Whisper invented 'Pony und Kleid' for 'Bonnie und Clyde')
- 99 languages supported, including SLO/HR/BS/SR
- $0.40/h pricing, ~$0.022 per 200s song

Implementation:
- transcribe_with_elevenlabs() function uses Scribe v1
- ISO 639-1 ↔ 639-3 mapping (Scribe needs 'deu' not 'de')
- Word-level timestamps converted to pseudo-segments (close on 0.6s pause or 6s duration)
- 24MB upload limit guard with auto-fallback to local

Default whisper_provider='auto':
- If ELEVENLABS_API_KEY set → use Scribe
- Otherwise → fallback to local faster-whisper
- 'elevenlabs' strict mode: no fallback
- 'local' strict mode: skip Scribe entirely

Tested on Ben Zucker - Ohne dich: Scribe correctly transcribed
'Wir sind Bonnie und Clyde, zu allem bereit' where local Whisper hallucinated.
2026-04-29 12:03:40 +00:00
3ffa9740f0 Revert "Add Groq Whisper API integration (200x faster than local CPU)"
This reverts commit 5c53a27d33.
2026-04-29 11:19:31 +00:00
5c53a27d33 Add Groq Whisper API integration (200x faster than local CPU)
Pipeline:
- New transcribe_with_groq() function uses Groq's whisper-large-v3-turbo
- 30s audio transcribed in ~0.5s (vs 30s+ on CPU)
- Same quality as local Whisper (it's the same OpenAI model)
- Cloudflare bypass via custom User-Agent header
- 24MB upload limit guard with auto-fallback to local
- Language auto-detect works (Groq returns full lang name, mapped to ISO codes)

Default whisper_provider='auto':
- If GROQ_API_KEY is set → use Groq (200x faster)
- Otherwise → fallback to local faster-whisper
- Strict 'groq' mode: no fallback (returns empty if Groq fails)
- Strict 'local' mode: skip Groq entirely

CLI: --whisper-provider {auto,groq,local}
API: whisper_provider field in StartJobIn

Cost: $0.04/h with whisper-large-v3-turbo ($0.002 per 200s song)
2026-04-29 11:08:15 +00:00
60765ad84c Anti-hallucination: filename hint to LLM + beam search + silence threshold
When Whisper hallucinates (generates fake lyrics not matching the audio),
LLM can now use the original filename as a hint to recognize the song
and override the false transcript with the actual lyrics.

Pipeline:
1. Pass filename (e.g. 'Ben Zucker - Bonnie und Clyde') as hint
2. Whisper transcribes (may hallucinate)
3. Claude/Gemini reads filename + transcript:
   - Recognizes song from filename hint
   - Compares Whisper output to known lyrics
   - Replaces hallucinated text with real lyrics (preserves timestamps)
   - If can't fix, removes segment (better silent than wrong)

Also added Whisper anti-hallucination params:
- beam_size=5 (more careful decoding vs greedy)
- hallucination_silence_threshold=2.0 (skip text in long silences)
2026-04-29 10:48:55 +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
OpenClaw Agent
0ca33be6ac Fix: clip_range source dynamic from LLM result instead of hardcoded 'claude'
Diagnoza:
- analyze.py je zgodovinsko imel samo Claude support
- ko se je dodal Gemini, je clip_range.source ostal hardcoded 'claude'
- prav tako log 'Whisper segmenti zamenjani s Claude' in 'Generated SRT from Claude'
- API rezultat je v jobu kazal source='claude' tudi ko je dejansko bil uporabljen Gemini
- to je samo COSMETIC bug — funkcionalno je vse delovalo pravilno
- Gemini se DEJANSKO klical (potrjeno: '🤖 Gemini (gemini-3.1-pro-preview) izbral: 172.5-201.8s')
  in vrnil pravilen rezultat — samo logging je rekel napačno

Popravki:
1. clip_range['source'] = claude_result['source'] (dejansko 'gemini:...' ali 'claude:...')
2. clip_range['reason'] prefix iz hardcoded 'claude_llm:' v dinamičen '{source}:'
3. Log 'Whisper segmenti zamenjani s Claude' → 'z {llm_label}'
4. Log 'Claude je popravil jezik' → 'LLM je popravil'
5. main.py 'Generated SRT from Claude' → 'from {llm_src}'

Test (Zlati Muzikanti - Le prijatelja bodiva, valček, 246s):
✓ Gemini dejansko izbere refren (172.5-201.8s)
✓ Whisper detektira sl (p=0.97 across 3 samples)
✓ Vseh 18 segmentov popravljenih
✓ Pipeline end-to-end deluje

Backward compat:
- transcript['claude_corrected'] in srt_from_claude variable name ohranjena
  ker že obstajajo v starih job state fajlih
2026-04-29 09:49:58 +00:00
534d710e8a Auto-resume jobs interrupted by container restart
When Coolify redeploys, the container is killed mid-job.
Now on FastAPI startup:
- Detect status=processing jobs from JOBS_DIR
- If input file exists and resume_attempts < 3, restart pipeline (status=queued)
- After 3 failed attempts, mark as error
- If input is missing, mark error immediately
- Track resume_attempts and last_resume_at for diagnostics

Run actual process_job in asyncio executor (sync function in thread)
so startup completes quickly and resume happens in background.

Resolves: 'Veseli Dolenci stuck' issue
2026-04-29 08:52:16 +00:00
32baf9cd45 Auto-resume: cleanup stuck jobs on container startup + GEMINI_API_KEY env
- @app.on_event(startup) marks all status=processing jobs as error after restart
- Process endpoint now clears chorus_error/interrupted_at on retry (retry-friendly)
- GEMINI_API_KEY added to Coolify env (Gemini 3.1 Pro now active)
- User can now choose Gemini in UI dropdown for analysis
2026-04-29 08:43:31 +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