PROBLEM: Ko je clip range premaknjen zelo daleč od LLM-jevega prvotnega
center-ja, klikni+Enter za zelen IN postavi rdeč OUT (in vice versa) —
ker se odločitev IN vs OUT temelji na 'initialCenter' (fiksen iz LLM
analize), ki ne predstavlja več trenutne uporabnikove pozicije.
Primer (Die Granaten — A Frau is a Frau):
- initialIn=0:50, initialOut=1:30 → initialCenter=1:10
- User razširi clip: trimStart=0:06, trimEnd=2:33
- User klikne pri 2:02, želi postaviti IN bližje začetku
- Stara koda: 2:02 > 1:10 → postavi OUT (NAROBE, ker je playhead daleč
desno od dejanskega center-ja trim range-a)
FIX: Odločitev IN vs OUT po BLIŽINI playhead-a do trim handle-a:
- Bližje trimStart → IN marker
- Bližje trimEnd → OUT marker
Plus: clamp markerjev znotraj trim range-a (vsaj 0.5s razlika med IN/OUT)
Plus: počisti staro initialCenter logiko v onPointerMove (handle drag)
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
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
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š.
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'.
Prej: max-width 1100px, 1fr/1fr grid → na velikih ekranih je bil
levi 'Nov reel' panel zelo otesnjen, desni list pa premajhen.
Zdaj:
- main max-width 1600px (več prostora na velikih ekranih)
- levi panel fiksno 440px (vsebina diha + tabi v eni vrstici)
- desni jobs list zavzema vso preostalo širino (1fr)
- Responsive: <1100px → levi 380px, <900px → stack 1 stolpec
- Tabi: Vse / FOLX SLOVENIJA / FOLX DE / ONE DE / ZWEI MUSIC / ADRIA / (brez postaje)
- Števec na vsakem tabu = število NEPOTRJENIH jobov (!hidden_after_upload) za to postajo
- Klik na tab = filter samo tisti station, samo nepotrjeni
- 'Vse' = vsi nepotrjeni (kot do zdaj)
- '(brez postaje)' tab se skrije, če ni jobov brez postaje
- Persist v localStorage (reels_jobs_station_filter)
- Iskanje IGNORIRA station filter (vrne tudi naložene + vse postaje)
- Empty state sporočilo prilagojeno glede na izbran filter
User: 'če razširim zoom in podaljšam komad potem polovica ni več tam
kjer je reel IN in tam kjer je OUT. Naj se program drži polovice
avtomatično narejenega reela'
Pred (bug):
center = (trimStart + trimEnd) / 2 # dynamic
Ko user razširi clip 60-90 → 50-110:
nov center = 80 (prej 75)
Klik pri 78s → IN (čeprav user mislil OUT!)
Po (fix):
initialCenter = (startInit + endInit) / 2 # FIKSEN, izračunan ob open
Original LLM center se NE spreminja
Klik pri 78s vedno → OUT (če je 78 > 75)
Klik pri 70s vedno → IN
Ne glede koliko user razširi/oža clip, marker assignment uporablja
fiksno 'sredino' originalnega LLM-jevega clipa.
Tudi drag handle: če marker zaide čez initialCenter → reset na trim border.
User: 'še vedno mi skače zeleni trikotnik na konec! Naredi da ne gre
čez polovico ne en ne drug'
Bug analiza: prej Math.min(t, center - 0.1) NI bilo, samo direktna
assignment. Če bi user nekje napačno klikal, ali če je drag handle-a
spremenil center, marker je pristal na napačni strani.
Strogo zdaj:
- markerInTime = Math.min(t, center - 0.1) → zelen NIKOLI čez center
- markerOutTime = Math.max(t, center + 0.1) → rdeč NIKOLI čez center
Plus: po drag-u handle-ja se markerji preverijo:
- Če je markerIn na desni strani novega centra → reset na trimStart
- Če je markerOut na levi strani novega centra → reset na trimEnd
Plus console.log za debug — vidiš v Dev Tools kateri vrednosti uporabljam.
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)
User feedback: 'če kliknemo na konec se premika IN, in ne OUT.
Če je bližje OUT ko kliknemo, se mora premikati OUT trikotnik —
tudi če nismo prej izbrali IN. Če je IN ok potem tega ne delamo.'
Pred (bug):
distToLeft = |t - trimStart|
distToRight = |t - trimEnd|
Če enaka razdalja → IN (default '<=')
Ko si že popravil IN, distance lahko enaka → klik blizu OUT
pomotoma premakne IN
Po:
center = (trimStart + trimEnd) / 2
if t < center → IN
else → OUT
Logika: leva polovica clipa = IN domain, desna = OUT domain.
Klik kjerkoli blizu desnega handle (= konec) bo VEDNO premaknil OUT.
Klik kjerkoli blizu levega handle (= začetek) bo VEDNO premaknil IN.
Tudi če si že popravil IN, popravljanje OUT ne bo motilo IN markerja.
User: 'zakaj se je ustavilo? Naložil sem več kot 70.'
Diagnoza: 12/70+ je prišlo do server-ja (vsi 200 OK).
Browser-side problem: en upload je stuck → cel for-loop blokiran.
Fixes:
1. xhr.timeout = 10min per file (prej: večnost)
2. xhr.ontimeout, xhr.upload.ontimeout — proper error handling
3. NEW: uploadFileWithRetry() — 2x retry z 2s/4s eksponentnim delay-om
za očasne mrežne odpovedi
4. Catch v loop ne kliče liveFail() (kar bi naredil disabled submit-btn)
ampak samo showLive() z 'preskočil' sporočilo → loop nadaljuje
5. Console.warn v retry attempts za debugging
Sedaj če eno failes:
- Retry 2x avtomatsko (2s + 4s delay)
- Če še vedno ne uspe → preskoči to datoteko, nadaljuj z naslednjo
- Pri koncu vidiš katere so failed (v console + showLive)
User: 'sej je že preset narejen, ne kompliciraj. Vse je enako za reels.
Naredi da so podnapisi po default GOR (brez kljukice). Klukica = brez napisov.
Ostalo zapeči, nimamo kaj spreminjat.'
Hidden defaults (zapečen preset za vse reels):
- mode=track
- quality=medium
- llm-provider=claude
- whisper-model=large-v3
- subtitle-style=reels
- auto-chorus=true (data-checked)
- include-prebuild=false (data-checked)
- duration=30
Vidno samo:
- TV postaja (tabs) — kam gre na Nextcloud
- Brez kljukice = napisi V VIDEO (default)
- Kljukica = brez napisov
Side note: 'Brez kljukice = napisi' je obratno od prej (kljukica = brez).
Default je sedaj 'with subtitles' (no-subs unchecked). Persistence v
localStorage ohranja izbiro med reload-i.
Bonus: odstranjen 'Auto-chorus toggle' handler ki je iskal #manual-times
ki ne obstaja več.
User feedback: 'ko refrešam se ponovno vklopi kljukica da ni
podnapisov to bo problem'
Saved fields:
- no-subs (checkbox)
- auto-chorus (checkbox)
- include-prebuild (checkbox)
- mode (track/center/blur)
- quality (fast/medium/high)
- llm-provider (claude/gemini/auto)
- tv-station (FOLX SLOVENIJA / ONE DE / ...)
On page load: re-aplicira saved values
On change: shrani v localStorage
TV station: tudi posodobi aktiven tab style
Defaults ostanejo isti če nikoli niso spremenjeni.
User feedback: 'pa ne maram teh emojijev ki si ji dal k Adria in ZWEI Music.
Daj počisti sedaj kar je že narejeno in gremnalagat.'
UI changes:
- Tab labels: 'FOLX SLOVENIJA', 'FOLX DE', 'ONE DE', 'ZWEI MUSIC', 'ADRIA'
(without emoji prefixes)
- Job card badge: brez emoji prefix
Cleanup (manual via container):
- /data/jobs/*.json (15 → 0)
- /data/outputs/* (132 → 0)
- /data/uploads/* (15 → 0)
Dedup baza je ohranjena (15 zapisov), tako da če uporabnik poskuša
naložiti komad ki je že naložen na Nextcloud, dobi opozorilo.
User feedback: 'ko ni kljukice so napisi, zato napiši samo izklopi
podnapise in ko je kljukica so izklopljeni'
Pred: 'Brez podnapisov (privzeto — bolj zanesljivo)'
Po: 'Izklopi podnapise (kljukica = brez napisov · brez kljukice = napisi v video)'
Logika ostane ista (no_subs flag), samo label je bolj jasen.
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)
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
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.
User feedback: 'tukaj imava cel kup stvari ki niso res, kako oblikujemo?'
Old text was misleading:
- 'Whisper, 3-sample voting' → not used since Soniox integration
- 'Model: medium' → irrelevant (Whisper not used)
- 'Whisper + energy → najde refren' → now Soniox + Claude LLM
New text reflects actual stack:
- STT: Soniox (primary) → ElevenLabs Scribe → Gemini fallback
- LLM: Claude Sonnet 4.6
- Energy profile + word-level timestamps + 15 reference examples
- Mention ✏️ Edit button for manual fine-tuning
User feedback: 'ko hočemo preveriti konec, predvajaj samo 5s.
začetek ni problematičen ker plej začne od začetka'
NEW button: ▶ Konec (5s) — green
- Seeks to (trimEnd - 5s)
- Plays from there to trimEnd
- Auto-stops at trimEnd (existing logic)
- Quick way to verify if 'OUT' position is correct without
waiting for full clip playback (which can be 30-60s)
Renamed: '▶ Predvajaj odsek' → '▶ Predvajaj cel' for clarity
(plays full clip from start to end)
Workflow now:
- Adjust handles
- '▶ Predvajaj cel' to hear whole clip (when needed)
- '▶ Konec (5s)' to quickly check if end is right
- Iterate handles until perfect
- Save
Bug: triangles positioned at top:-14px were outside trim bar bounds.
Trim bar has overflow:hidden, so triangles were clipped (invisible).
Fix: top:0 (inside trim bar, at the very top edge).
Triangle 14px tall now sits at top of trim bar (overlapping waveform
slightly but visible, with drop-shadow to make them stand out).
User feedback: Workflow is - click + Enter sets a marker triangle, then
button moves the red handle to that triangle. Triangle near LEFT handle
= IN candidate (green), near RIGHT = OUT candidate (red).
Visual:
- Green triangle (▼) above trim bar = IN candidate position
- Red triangle (▼) above trim bar = OUT candidate position
- White line (playhead) = current video position (moves during playback)
- Red handles (existing) = actual clip start/end
Workflow:
1. Click on waveform → white playhead jumps there
2. Press Enter → playhead starts moving (plays)
ALSO: triangle gets placed at current position
- If position closer to LEFT handle → green IN triangle
- If position closer to RIGHT handle → red OUT triangle
3. Listen, decide 'this is the right spot'
4. Click ▼ Postavi IN button → red LEFT handle jumps to green triangle
(or ▼ Postavi OUT for right handle)
5. Now red handle and triangle are aligned = clip boundary committed
Triangles persist until next play press (= next candidate).
Buttons styled with matching color (green for IN, red for OUT).
User feedback: 'naj ne začne takoj predvajati. naj začne ko pritisnem
Enter, in pozicija naj ostane črta ker bomo tja dali tracker'
Changes:
- Click on waveform: just seek + render playhead (was: seek + auto-play)
- Click on segment row: just seek + render playhead (was: seek + auto-play)
- Playhead: brighter, with triangle marker on top (tracker placeholder)
- Enter key: play/pause toggle from current position
- Space key: also play/pause (back-compat)
- Hint texts updated to reflect new workflow
Workflow now:
1. Click on waveform/segment → playhead jumps there (no sound)
2. Read transcript, look at waveform around the position
3. Press Enter → plays from there
4. Press Enter again → pauses
5. Click somewhere else → playhead moves there (paused)
6. Press Enter → plays from new position
Allows precise positioning before commit to playback.
User feedback:
1. 'Wave form je premajhen — zoom'
2. 'Ko nastavimo pozicijo, play od začetka — ne moremo predvajat od tam'
NEW Zoom feature:
- 5 zoom levels: 1x, 2x, 5x, 10x, 20x
- Trim bar wrapped in scrollable container
- On zoom: bar width grows to 100*N%, scroll auto-centers on trim region
- Higher zoom = more pixels per second = micro-tuning possible
(1x: 5px/s, 20x: 100px/s for 4min song)
- Active zoom button highlighted accent red
NEW Play-from-position:
- Click on waveform/trim bar = playhead JUMPS THERE + auto-plays
(was: just moved playhead, no play)
- Space key = play/pause toggle from current position
(works anywhere except in input fields)
- '▶ Predvajaj odsek' still does start-to-end of selection
- Cleanup keydown listener on modal close
Waveform now rendered at 2400x72 (higher res) so zoom looks crisp.
User can now:
- Zoom 10x to see exact word boundaries in waveform
- Click anywhere → instant play from there
- Hit Space to toggle while watching
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
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
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)
- Big '▶ Predvajaj odsek' button: plays from trim start
- Auto-stop when video reaches trim end (loops back to trim start)
- iPhone trim preview behavior: see exactly what reel will contain
Screenshot revealed: trim bar element has only 4px width even after
ResizeObserver fires. Likely the parent (.modal-content) is a flex
container that shrinks the trim-bar.
Force trim bar to take full width with width:100% and prevent shrinking
with flex-shrink:0.
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
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.
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
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
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.
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).
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.
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.
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
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
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.