- 2026-04-23: Se corrigió un último flash visible al auto-avanzar entre clips de modos distintos (síntoma reportado: del clip 1 al darle play, el clip B2 aparece como "modo fit" pero con la posición del video adentro mal — como si siguiera en fill — y al pausar se acomoda). Dos causas: (1) en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `shouldRenderFitPreviewBackdrop` solo era `true` cuando el clip seleccionado actualmente era `fit`, así que cuando la transición era `fill→fit` el `<video>` del backdrop blur no estaba montado en el DOM al momento de la pre-rolada de staging; `getStagingBackdropVideo()` devolvía `null`, la promesa `stagingReadyPromise` se resolvía vacía y el backdrop aparecía recién varios frames después del swap. Fix: añadido el memo `sequenceHasAnyFitBlock` que recorre `derivedBlocks` resolviendo cada modo via `resolveClipDisplayModeForAspect(... , previewAspectPreset)`; el backdrop se monta cuando el clip actual es fit O cuando cualquier bloque de la secuencia lo es, garantizando un elemento real para precalentar antes de cada transición hacia fit. (2) En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), tras el `await syncSecondaryPlayback(...)` del path de auto-avance, se levantaba `setPreviewModeTransitionPending(false)` inmediatamente — pero ese flag controla la clase `is-syncing` que pone `opacity:0` sobre el `<video>` primario, así que al quitarlo no había ninguna garantía de que React hubiera commiteado el className/region-class/frameStyle/videoStyle del próximo clip ni de que el compositor hubiera pintado un frame con esa nueva geometría antes de revelar el video. El primer paint visible podía ser el video del clip nuevo metido dentro del shell del clip viejo (fit estirado a dimensiones de fill). Fix: justo antes de `setPreviewModeTransitionPending(false)` se aguardan dos `requestAnimationFrame` para que React procese sus setStates y el browser pinte al menos un frame con el modo/region/frame/video resueltos, con re-check de `playbackRef.current.session/.mode` para abortar limpio si el usuario detuvo durante la espera. Coste visual: ~32 ms extra (≈2 frames @ 60Hz) antes de que el clip nuevo sea visible — imperceptible vs. el flash anterior. Detalles guardados en `/memories/repo/clip-transition-mode-pre-resolve.md`. esbuild + Pylance limpios en ambos archivos editados.
- 2026-04-22: Continuación del fix de transición clip→clip. El primer parche (frame staging con su propio React state `stagingSlotFrameClassName/Style`) cubría sólo la mitad del bug. Captura Playwright durante reproducción mostró un segundo defecto: durante ~290ms tras una transición fill→fit, el contenedor `.workflow-preview-primary-shell` ya tenía className `mode-fit` (escrito imperativamente por `flushPendingDisplayMode`), PERO el slot primary visible seguía siendo el slot A con su `<div>` frame en `.workflow-preview-fill-frame` (esperando el render de React) Y SU `<video>` con `style.width/height/transform/objectPosition` ya BORRADOS imperativamente por el bloque de `staleKeys` dentro del propio `flushPendingDisplayMode`. Resultado: el video del slot A seguía visible, pero sin sus estilos de crop, dentro de un shell `mode-fit` (que pierde el `overflow:hidden` del fill), por lo que el video escalado de fill se desbordaba y se veía "agrandado sin posicionarse" hasta que React rendereaba y consolidaba la swap. Pausar el preview detenía el rAF loop y el siguiente render corregía. Causa raíz: las mutaciones DOM imperativas (`regionEl.className = ...` y `oldVideoEl.style[k] = ""`) dentro de `flushPendingDisplayMode` se aplicaban **inmediatamente** mientras los `setState` (`setPlaybackOverrideDisplayMode` + `setPrimaryPlaybackVideoStyle` + el `setPrimaryVideoSlot` que viene después en el transport hook) sólo se reconcilian cuando React rendea — dejando una ventana inconsistente de varios frames antes del próximo paint. Fix en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `flushPendingDisplayMode`: (1) eliminado el bloque que escribía `regionEl.className` (ahora React lo aplica vía `previewPrimaryResolvedFrameClassName/...mode-fit/-fill` que reacciona a `playbackOverrideDisplayMode` y se rendea atómicamente con `setPrimaryVideoSlot`); (2) eliminado el loop `for (const k of staleKeys) oldVideoEl.style[k] = ""` para `objectPosition/transform/transformOrigin/width/height/objectFit` — React ya los limpia automáticamente en el diff de `style` cuando el slot pasa de `resolvedPrimaryVideoStyle` (estilo imperativo aplicado durante prep, persistido en `primaryPlaybackVideoStyle` state) a `EMPTY_VIDEO_STYLE` al volverse staging. (3) Conservado el `oldAnimRef.current.cancel()` (la WAAPI animation hay que cancelarla a mano) y el `removeProperty()` para los CSS custom props `--crop-w/--crop-h/--crop-tx/--crop-obj-pos/--crop-tx-fill` (React no conoce esas custom props y no las limpia). Build esbuild limpio. Validación visual queda al usuario reproduciendo cualquier secuencia con clips de modos mixtos — la transición debe ahora ser atómica sin "agrandado" intermedio.
- 2026-04-22: Se corrigió que al pasar de un clip al siguiente durante reproducción, el clip nuevo apareciera "raw" / sin conformarse al encuadre por uno o más frames antes de reposicionarse. Causa: en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) los dos slots `<video>` (A y B) se renderizaban dentro de un `<div>` frame que **siempre** consumía `previewPrimaryResolvedFrameClassName` y `resolvedPrimaryFrameStyle`, ambos derivados del **clip primary** (modo fit/fill/split actualmente visible). Sólo la `<video>` interna del slot staging tenía className propio (`stagingSlotVideoClassName`). Durante la prep del slot staging, `applyClipStylesImperatively(...)` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) escribía imperativamente `frameEl.className = nextFrameClass` y `Object.assign(frameEl.style, frameStyle)` con las dimensiones/clase del **nuevo** modo, leía `frameEl.offsetWidth/Height` y computaba `videoStyle` (transform/objectPosition) en base a esas dimensiones. Pero el siguiente render de React (que ocurre cada rAF tick durante playback porque `setSequencePlayheadMs` actualiza estado) sobreescribía esa className/style imperativa del frame staging volviendo a la del primary, dejando el video staging con un transform calculado para dimensiones que ya no eran las que el frame realmente tenía. Cuando el swap de slots ocurría (`primaryVideoSlot` flip), el frame del nuevo primary recobraba el className correcto sólo después de que React procesara `flushPendingDisplayMode → setPlaybackOverrideDisplayMode`, dejando 1+ frames con dimensiones/transform desalineados. Fix: (1) En [DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadieron dos `useState`s nuevos al lado de `stagingSlotVideoClassName`: `stagingSlotFrameClassName` (default `"workflow-preview-fit-frame preview-editable-surface"`) y `stagingSlotFrameStyle` (default `{}`), y `applyClipStylesImperatively(...)` ahora, **además** de escribir el frame imperativamente, llama `setStagingSlotFrameClassName(nextFrameClass)` y `setStagingSlotFrameStyle(frameStyle || {})` cuando `isStaging` es true, mirroreando lo que ya hacía con `stagingSlotVideoClassName`. (2) En el sitio donde se renderiza `<ClassicEditorPreviewPanel>` se añadieron los nuevos props `stagingSlotFrameClassName` y `stagingSlotFrameStyle`. (3) En [ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) se aceptan los dos nuevos props y los dos `<div>` frame de los slots A/B ahora usan `primaryVideoSlot === "a"|"b" ? previewPrimaryResolvedFrameClassName : (stagingSlotFrameClassName || previewPrimaryResolvedFrameClassName)` y, para `style`, `primaryVideoSlot === "a"|"b" ? resolvedPrimaryFrameStyle : { ...(stagingSlotFrameStyle || resolvedPrimaryFrameStyle), opacity: 0, pointerEvents: "none" }`. Con esto el frame staging mantiene la className+style del **modo del clip que va a entrar** durante toda la preparación, los `videoStyle` computados en `applyClipStylesImperatively` siguen siendo coherentes con sus dimensiones, y al hacer swap el nuevo primary ya está correctamente conformado desde el primer paint. Build esbuild limpio en ambos archivos modificados, sin nuevos errores de Pylance/ESLint en los rangos editados. Validación visual queda al usuario reproduciendo cualquier secuencia con clips de modos mixtos (fit→fill o fill→fit) — el clip nuevo ya no debe verse "de corrido sin esclarar".
- 2026-04-21: Se corrigió que las muestras automáticas de auto-frame no se aplicaran al preview principal cuando un clip no tiene keyframes manuales (el modal sí las animaba pero el `<video>` quedaba con `objectPosition: 50% 50%`/`transform: none`). Causa: el campo `autoCropKeyframes` que `resolveEffectiveCropKeyframes` lee como fallback nunca era escrito por el pipeline de auto-frame; por tanto `cache.cropKeyframes` quedaba vacío y `updatePlaybackCropFrame` salía temprano, mientras que el modal calculaba su propia `smoothedTrajectory` vía `buildSmoothedTrajectory(mappedDetections)` y aparentaba funcionar. Fix en runtime (no se cambia la persistencia): (1) en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se importó `buildSmoothedTrajectory` desde `../autoFrameTrackingFilter` y se añadió el helper module-scope `deriveAutoCropKeyframesForBlock(block, displayMode, baseCropRect, autoFrameDiagnostics, ignoredAutoFrameSampleTimes)` que: localiza las detecciones del bloque (preferencia per-block `autoFrameDiagnostics.blocks[].frame_detections`, fallback a `autoFrameDiagnostics.global_frame_detections` filtrado por `source_in_ms..source_out_ms`), aplica el filtro `filterAutoFrameDetectionsByIgnoredSamples`, mapea cada `t` de tiempo de fuente a tiempo local del clip, construye la trayectoria suavizada con `buildSmoothedTrajectory(..., { tolerance: 0.5 })` (mismo `tolerance` que el modal), y traduce cada punto en `{timeMs, cropRect}` con la misma matemática de `liveCropRectStyle` del modal (`liveX = clamp(cx - w/2, 0, 1-w)`, `liveY = isFillMode ? baseCropRect.y : clamp(cy - h/2, 0, 1-h)`). (2) Se modificaron los dos call sites donde se construyen `cropKeyframes` para playback (la prep al cambiar de bloque alrededor de la línea 4997 y el cache rebuild dentro de `updatePlaybackCropFrame` alrededor de 5238): si `resolveEffectiveCropKeyframes(stored?.[displayMode])` devuelve vacío y el modo es `fit` o `fill`, se rellena con `deriveAutoCropKeyframesForBlock(...)` antes de `normalizeCropKeyframes`. Con esto la WAAPI animation (fit) y el rAF loop con CSS custom properties (fill) ya tienen ≥2 keyframes y dejan de salir por la rama estática. El modal sigue intacto: ya animaba correctamente, ahora el preview principal hace exactamente lo mismo. Validación Playwright sobre `?project=25&sequence=editorial-approved-1775805345290-thematic-20-85848-split-2-sub-split-2`: durante reproducción del clip "con control de tracción..." (fill, 4 muestras auto, 0 manuales) el `<video>` pasó de `objectPosition: 50% 50%` a `100% 49.9999%` siguiendo la trayectoria; del clip "esta versión cuenta con un motor..." (fill, 6 muestras auto, 0 manuales) `objectPosition` se estabilizó en `21.1525% 50%`. esbuild + lint limpios.
- 2026-04-21: Se corrigió el bug de "el título principal salta a la esquina arriba-izquierda al empezar a moverlo" (drag overlay del titulo y subtítulo en el preview clásico). Causa raíz: corrupción `null → 0` en round-trip JSON. Cuando una secuencia no tenía posición persistida (`mainTitlePositionX/Y`, `subtitlePositionX/Y`, `mainTitleFontScale`, `mainTitleWidthScale` todos `null`), la rehidratación en [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) usaba `Number.isFinite(Number(value))`. `Number(null) === 0` y `Number.isFinite(0) === true`, así que el helper guardaba `0` en lugar de mantener `null`. Tras eso, el resolver en `DashboardPage.jsx` aceptaba `0` como valor válido (`Number.isFinite(0) === true`), saltándose el fallback al default `0.5/0.10`. El render dibujaba el título en la esquina superior izquierda (clamp inferior 0.04), el ref `resolvedOverlayPositionsRef.current.titleX/Y = 0`, y al hacer drag el `Math.max(0.04, drag.startX + delta)` clampeaba a 0.04 → el título "saltaba" a top-left en el primer pointermove. Fix en tres pasos: (1) cambié las cinco invocaciones de `Number.isFinite(Number(x)) ? Number(x) : null` que normalizan estos campos en `dashboardSequenceModelHelpers.js` y en `DashboardPage.jsx` (snapshot serializado en `mappedSequences`) por una versión que primero rechaza `null/undefined/""`. (2) En el resolver del componente (líneas 4058-4067) `storedTitleFontScale/WidthScale` y `storedTitlePosition*`/`storedSubtitlePosition*` ahora rechazan valores corruptos fuera del rango operativo (`fontScale >= 0.75`, `widthScale >= 0.7`, posiciones en `[0.04, 0.96]`), de modo que sequences ya corrompidos en disco con `0` también caen al default sin necesidad de migración. (3) Mismo patrón aplicado en [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) para que el slide 4 muestre los títulos en su posición correcta. Validación Playwright sobre `?project=25&sequence=editorial-approved-1775805345290-thematic-20-85848-split-2-sub-split-2`: el título arranca en `left:156px; top:55px` (default 0.5/0.10), drag de +10px X mueve a `left:166px; top:55px` (sin salto), drag amplio +80px X / +200px Y mueve a `left:266px; top:255px` (1:1 pixel-perfect respecto al delta del cursor). El bug afectaba también a subtitle (mismo patrón con los mismos cuatro fields). Sin errores en los tres archivos editados.
- 2026-04-21: Se añadió soporte de split en el modal de crop + botonera "Full auto / Semi auto" por clip (fase 1, solo modal+storage). El modal de crop antes solo presentaba una caja editable y un aspect ratio, incluso cuando el `selectedClipDisplayMode === "split"`, a pesar de que `selectedClipAdjustment` ya venía con `{primary, secondary}` y el preview/export ya respetaban ambos slots. Cambios: (1) En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el `PreviewCropModal` ahora acepta los props `clipDisplayMode`, `selectedSplitPanel`, `onSelectSplitPanel`, `splitSecondaryAdjustment`, `splitSecondaryCropKeyframes`, `splitSecondaryCropRectStyle`, `onFullAutoClip`, `onSemiAutoClip`, `isSemiAutoActive`. Si está en split, junto al aspect ratio aparece un toggle A/Arriba—B/Abajo que refleja qué slot se está editando (con contador de KF por slot); todo lo que ya consumía `selectedPrimaryAdjustment` sigue sirviendo porque `DashboardPage` enruta ese prop al slot activo. Además se añadió la botonera "Full auto" / "Semi auto" (unconditional, también sirve fit/fill). En el canvas del modal, cuando hay split, se renderiza un segundo `.workflow-crop-selection-rect--inactive` con el box contrario en naranja y dashed, que al click cambia el slot activo (pointer events propios, sin handles). (2) En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadieron los memos `splitSecondaryCropKeyframes` y `splitSecondaryCropRectStyle`, el flag `isSemiAutoActive = Boolean(selectedPrimaryAdjustment?.preferSemiAuto)`, y dos callbacks: `handleFullAutoClip` delega en `handleClearCropKeyframesForBlock(previewEditingBlock?.id)` (que ya es slot-aware en split y borra solo la slot activa), y `handleSemiAutoClip` escribe/borra `preferSemiAuto: true` en `currentAspectSettings[selectedClipDisplayMode]` (o en `split[selectedSplitPanel]` si estamos en split) vía `updateActiveSequence + resolveClipAdjustmentBucketForAspect + assignClipAdjustmentBucketForAspect`, marcando el blockId en `manualClipDisplayBlockIds`. El bloque `<PreviewCropModal>` ya pasa estos nuevos props. (3) En [frontend/src/styles/app.css](frontend/src/styles/app.css) nuevas reglas para `.workflow-crop-split-slot-toggle`, `.workflow-crop-split-slot-button(.is-active)`, `.workflow-crop-split-slot-badge`, `.workflow-crop-auto-actions`, `.workflow-crop-auto-action-button(.is-active)`, `.workflow-crop-selection-rect--inactive` y `.workflow-crop-selection-inactive-badge`, con esquinas casi rectas (`border-radius: 3–4px`) siguiendo la preferencia del usuario. El "Full auto" abre un `window.confirm` con mensaje adaptativo ("Borrar los keyframes manuales de la caja B (abajo) de este clip...") antes de invocar el handler. Lo que queda pendiente para una fase siguiente: cablear `preferSemiAuto` en la interpolación real del `resolveCropRectAtTime` y en el pipeline de export (hoy solo se persiste el flag en el draft). Validación Playwright sobre `?project=25&sequence=editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1`: abrir el clip "Empieza el precio desde Q.126,000.00" (auto-frame H split→split), el modal ahora muestra A/B + Full/Semi, click en B activa `aria-pressed=true` sobre el slot B y el badge del rect inactivo flipea a "A", toggle Semi auto pasa a `is-active` + `aria-pressed=true` y el contador del panel de Encuadres sube de "1 manual" a "2 manual" (persistencia confirmada), y Full auto dispara el confirm "Borrar los keyframes manuales de la caja B (abajo) de este clip y dejarlo en auto total?" (mensaje slot-aware correcto). Sin errores de consola ni lint nuevos en los archivos editados.
- 2026-04-21: Se restauró la función de "doble click en una palabra del transcript para corregirla", que estaba parcialmente cableada pero rota. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ya existían el `useState` `transcriptWordEditor`, los `useRef`s del input y popover, los `useEffect`s para autoseleccionar el input/cerrar al click fuera/limpiar al perder la palabra, todo el JSX del popover (`.transcript-word-editor-popover` con form, input, hint y `Original: ...`), los estilos en [frontend/src/styles/app.css](frontend/src/styles/app.css), y los dos `onDoubleClick` (uno sobre `transcriptParagraphs` del script general y otro sobre las `word-token` de cada bloque del transcript de secuencia). Lo que faltaba eran las cuatro funciones que el JSX referenciaba: `openTranscriptWordEditor`, `handleTranscriptWordEditorChange`, `handleTranscriptWordEditorKeyDown` y `commitTranscriptWordEditor`. Sin ellas, el doble click lanzaba `ReferenceError: openTranscriptWordEditor is not defined` y nada se abría. Fix: se añadieron las cuatro como `useCallback` arriba del primer `useLayoutEffect` de `transcriptWordEditor`. `openTranscriptWordEditor(word, event)` resuelve `sourceWordId` desde `word.sourceWordId ?? word.id`, lee el original desde `originalWordsById`, calcula posición flotante centrada bajo el `currentTarget` y propone como valor inicial el override actual o el texto original. `handleTranscriptWordEditorChange` solo actualiza `value`. `handleTranscriptWordEditorKeyDown` mapea `Escape` → cerrar, `Enter` → commit, y **`Backspace` con todo el input seleccionado** → borra el override y cierra (este es el "borrar para volver al original" que pidió el usuario). `commitTranscriptWordEditor` normaliza espacios, y si el resultado es vacío o coincide con el original elimina la entrada de `transcriptWordOverrides`; si difiere, la setea. Validación Playwright sobre la URL de la secuencia editorial activa: doble click en `¿Hablemos` abre popover con `Original: ¿Hablemos`, escribir `Conversemos` + Enter deja la palabra como `Conversemos` con clase `edited-copy`, reabrir + Backspace (con texto auto-seleccionado) restaura `¿Hablemos` y quita `edited-copy`. El override persiste en `transcriptWordOverrides` y se serializa en el draft via `saveProjectDraft`/`persistEditorDraft` (los efectos existentes ya escuchan `transcriptWordOverrides`).
- 2026-04-21: Se corrigieron dos bugs al abrir la secuencia `editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1` del proyecto 25. (1) Rehacer master innecesariamente: en [backend/apps/editor/views.py](backend/apps/editor/views.py) `ProjectSequenceMediaPreparationView.post` no contemplaba el caso en que `editor_draft.sequenceMedia` estuviera vacío pero `master.mp4` y `proxy.mp4` ya existieran físicamente en `media/timeline/project_{id}/sequence_media/{media_key}/`, con lo que siempre disparaba el worker ffmpeg. Se añadió un short-circuit antes de `_start_sequence_media_preparation_worker`: si no hay run activo (`processing`/`queued`) y el master existe en disco, se construye un snapshot sintético (preservando `sourceClips`, inyectando `sequenceMedia.mediaKey` y `label`) y se responde 200 con el estado sintetizado vía `_synthesize_media_preparation_ready_from_disk`. Validación Playwright sobre la URL objetivo: el POST a `/sequence-media-preparation/` respondió `200 789` (sin worker), `proxy.mp4` se sirvió inmediato desde disco y no aparecieron `master.*tmp.mp4` en `media-seq-gjrk7i/`. (2) Crop modal ignoraba keyframes auto cuando no había manuales: `liveCropRectStyle` en [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) ya tenía la rama fallback a `smoothedTrajectory` construido con `buildSmoothedTrajectory(mappedDetections)`, pero llamaba `sampleTrajectoryAtTime(smoothedTrajectory, videoCurrentTime * 1000)`. Los `mappedDetections` están en **tiempo de secuencia editorial**, no en tiempo del proxy/master, así que el lookup no encontraba muestras válidas (o devolvía la equivocada) y el rect no se trasladaba. Fix: sustituir por `currentSequenceTimeMs` (variable ya existente que convierte `videoCurrentTime` → tiempo editorial usando `playbackSequenceBlock` y `sequence_start_ms`) y actualizar las deps del `useMemo`. Con 13 clips auto-analizados y 0 keyframes manuales en el clip abierto, el modal indica "Sin keyframes manuales, el clip sigue el auto frame · 2 muestras automáticas · fill" y ahora el rect se mueve con el sujeto durante playback.
- 2026-04-21: Se replanteó el control de volumen del preview clásico para que el slider siempre esté visible y funcione con todas las interacciones típicas. Antes, en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el slider vivía dentro de un popover (`volumeSliderOpen` + `.sequence-preview-volume-slider-shell`) que sólo aparecía al hacer click en el botón de altavoz, y su CSS en [frontend/src/styles/app.css](frontend/src/styles/app.css) forzaba `writing-mode: vertical-lr; width: 14px; height: 116px` incluso en el modo horizontal, dejando un rectángulo inutilizable. Cambios: (1) El cluster ahora es un inline-flex siempre visible con `[mute button] [slider horizontal 60–84px] [porcentaje]`, donde el porcentaje solo aparece en `density-full` y el slider se oculta en `density-micro`. El botón de altavoz alterna mute directamente (`onToggleMute`), con `aria-pressed` reflejando el estado mute. (2) Se añadió rueda del mouse sobre el cluster (listener nativo con `passive: false` en `useEffect` para poder `preventDefault()` sin el warning de React), que ajusta en pasos de 5%. (3) Se añadió navegación de teclado con Arrow Up/Down/Left/Right sobre el botón de altavoz (5% steps). (4) El slider usa `linear-gradient` sobre `--sequence-preview-volume-fill` para pintar el tramo ocupado en azul (`#4f83bd`) en WebKit y `::-moz-range-progress` en Firefox. (5) El slider refleja `muted ? 0 : volume` en su value, y `onVolumeChange` ya venía con auto-unmute en [DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Validación Playwright: click al botón mute/unmute alterna `video.muted`, drag del slider desde 0 auto-unmute y pone `video.volume=0.6`, la rueda sube/baja en 0.05, sin errores de consola ni warnings de passive listener, el cluster mide 98×26 px en compact density con slider horizontal de 60×18 px visible.
- 2026-04-21: Se corrigió que pulsar `Estándar` (o `Suave`/`Intenso`) pareciera "no activar" nada pese a que el proxy estabilizado ya estaba listo y marcado `ready` tanto en `stabilizationJobs` como en el `editor_draft`. Causa: en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `_buildModeStabilizationState(...)` comparaba las firmas con `!==` estricto, pero `mergedState.sourceSignature` venía con `range_end_ms` entero (`3242495`, derivado del backend que redondea con `int(round(float(...)))`) mientras `currentSequenceAudioEnhancementSourceSignature` se construye desde los bloques vivos con el mismo valor fraccionario (`3242494.804857106`). El mismatch disparaba la rama `status: "stale"`, con lo que `handleStabilizationModeClick(modeId)` en [ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) veía `modeStatus !== "ready"` y salía sin cambiar `sequencePreviewPlaybackMode` ni tocar `onSequencePreviewPlaybackModeChange`. Fix: se añadió el helper top-level `_areStabilizationSignaturesEquivalent(a, b)` en `DashboardPage.jsx` (parsea ambas firmas como JSON `{range_start_ms, range_end_ms}` y compara cada campo numérico con `Math.round`, fallback al compare literal cuando no son objetos), y `_buildModeStabilizationState` ahora usa ese helper en el stale-check. Simetría con el fix equivalente de backend (`_parse_range_signature` en `ProjectSequenceStabilizationView.post`). Validación: tras `page.reload()` el fiber de `ClassicEditorPreviewPanel` reporta `stabilizationModeStates.estandar.status === "ready"`, `sequencePreviewPlaybackMode === "estandar"`, el botón queda `aria-pressed="true"` y el `<video>` primario sirve `/media/timeline/project_25/sequence_media/media-seq-ypb8co/stabilized_estandar_proxy.mp4` con `duration: 222.293`.
- 2026-04-21: Se corrigió que pulsar `Estándar` (o `Suave`/`Intenso`) en una secuencia ya estabilizada relanzara el worker `vidstabdetect`+`vidstabtransform` aunque `stabilized_estandar.trf` y `stabilized_estandar_proxy.mp4` ya existieran físicamente en `media/timeline/project_25/sequence_media/media-seq-ypb8co/`. Causa: en [backend/apps/editor/views.py](backend/apps/editor/views.py) `ProjectSequenceStabilizationView.post` decidía si reutilizar el resultado de disco con `disk_sig == source_signature` (compare string-a-string), pero el frontend envía el rango con ms fraccionarios (`{"range_end_ms":3242494.804857106,...}`) mientras `_build_sequence_source_signature` redondea a entero (`3242495`). Los strings nunca coincidían, así que el endpoint disparaba un worker nuevo y la UI mostraba `Estándar` "procesando..." otra vez por encima del proxy bueno. Fix: la comparación ahora parsea ambas firmas como `{range_start_ms, range_end_ms}` con `int(round(float(...)))` y compara la tupla, con fallback al compare literal para firmas no-range. Validación: `POST /api/editor/projects/25/sequence-stabilization/` con `range_end_ms` fraccionario ahora responde `200` + `status: "ready"` (proxy y `.trf` reutilizados) en vez de `202` + `processing`. Recordatorio: backend corre con `--noreload` por defecto; reiniciar manualmente al editar `views.py`.
- 2026-04-21: Se restauró la reproducción y el scrubbing del preview clásico para secuencias cuya `editor_draft` no tiene `sequenceMedia` persistido (caso `editorial-approved-1775804275716-thematic-25-42962` → `media-seq-ypb8co`). Síntoma: pulsar Play colocaba el playhead "al inicio" pero el `<video>` quedaba congelado, y al scrubbear todos los frames eran prácticamente el mismo (siempre cerca del final del archivo). Causa: tras reiniciar el backend, `ProjectSequenceMediaPreparationStatusView` caía a `_synthesize_media_preparation_ready_from_disk(...)`, que devolvía `range_start_ms=0`/`range_end_ms=0` porque `normalized_media` venía vacío. El frontend usa esos campos como `getSequencePreviewRangeStartMs`, así que el transport calculaba `sequenceInMs = sourceInMs - 0 ≈ 3020 s`, muy por encima de la duración real del proxy (222.293 s), y los `<video>` se quedaban clamped al EOF. Fix en [backend/apps/editor/views.py](backend/apps/editor/views.py): el sintetizador ahora deriva el rango primero del JSON `source_signature` (que sí trae `range_start_ms/range_end_ms`) y, como segundo fallback, del mínimo/máximo de `source_in_ms`/`source_out_ms` de los `sourceClips` del snapshot, antes de caer al cero del payload normalizado. Validación: `GET /api/editor/projects/25/sequence-media-preparation-status/?sequence_id=...` ahora devuelve `range_start_ms: 3020215, range_end_ms: 3242495`, y la inspección DOM confirma `<video>.currentTime` avanzando (3.99 s, paused: false) con la barra mostrando `00:09.98`. Recordatorio: `start-backend.ps1` corre con `--noreload` por defecto, hay que reiniciar el backend manualmente para que el fix tome efecto.
- 2026-04-21: Mientras corre la estabilización de secuencia (`Suave`/`Estándar`/`Intenso`), el preview ya no salta a un modo estabilizado que aún no existe: queda reproduciendo `Original` con el proxy preparado para que el usuario pueda seguir trabajando durante el render. (1) En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) `handleStabilizationModeClick(modeId)` y `handleStabilizerClick()` ya solo llaman `onSequencePreviewPlaybackModeChange?.(modeId)` cuando ese modo está en estado `ready`; si está `idle`, `processing` o encolado, encolan/lanzan el render pero dejan el preview como esté (Original/proxy). El estado visual del botón sigue mostrando `is-processing` con `% progreso` o `is-queued`, y `autoSwitchOnReady: true` activa el modo estabilizado en cuanto termina. (2) En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleRunSelectedClipStabilization(...)` también dejó de llamar `handleSequencePreviewPlaybackModeChange(resolvedMode)` en la rama "queued" cuando todavía hay que generar master/proxy: el intent encolado más el flag `autoSwitchOnReady` ya cambian el preview al final. Validación: `npm run build` limpio (980 kB index, sin warnings nuevos). Resultado esperado: pulsar `Estándar` desde Original ya no produce el bache visual donde el preview cree estar en Estándar pero realmente no hay video estabilizado aún; la reproducción sigue corriendo con el proxy y el botón sólo se vuelve `is-active` cuando el render termina y la URL estabilizada está disponible.
- 2026-04-21: Se filtraron las secuencias desaprobadas del slide 4 (`BatchProjectExportPanel`) del library shell. El panel mostraba todas las secuencias del draft —incluidas las marcadas como `manuallyDisapproved` o ligadas a sugerencias en `disapprovedSuggestionIds`—, dejando entrar a la lista de export sequences que el usuario ya había desaprobado. Fix en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): antes de llamar a `buildBatchExportSequenceItems(...)` ahora se filtra `exportProjectDraft.sequences` con `resolveCreatedSequenceIsDisapproved(sequence)` cuando el proyecto exportado coincide con el live draft, y con `sequence.manuallyDisapproved/manually_disapproved` cuando la fuente es un bootstrap persistido de otro proyecto. Validación: `npm run build` limpio (980 kB index, sin warnings nuevos). Resultado esperado: el slide 4 de batch export muestra solamente las secuencias listas para exportar, las desaprobadas dejan de aparecer en la lista, el contador (`N secuencias`) y el `Seleccionar todas` ya operan sobre el subconjunto válido.
- 2026-04-21: Se corrigió que el botón `Estándar` (y `Suave`/`Intenso`) no arrancaba la estabilización de secuencia para `editorial-approved-1775804275716-thematic-25-42962` aunque `master.mp4` ya existía físicamente en `media/timeline/project_25/sequence_media/media-seq-ypb8co/`. Causa: el snapshot del `editor_draft` para esa secuencia no tenía `sequenceMedia` persistido (`None`), por lo que `ProjectSequenceStabilizationView.post` resolvía `sequence_media={}` con `master_relative_name=""` y `_resolve_sequence_media_source_path` devolvía `None` → `ValidationError("La secuencia necesita un video maestro listo antes de estabilizar.")`, y el frontend recibía 400 sin nunca lanzar el worker. Fix en [backend/apps/editor/views.py](backend/apps/editor/views.py): tras canonicalizar `media_key`, si `master_relative_name` está vacío pero `media/timeline/project_{id}/sequence_media/{media_key}/master.mp4` existe en disco, se sintetiza `sequence_media` con `master_relative_name`, `proxy_relative_name` (si proxy también existe), `status="ready"` y `source_signature` derivado de los `sourceClips` del snapshot, y se loggea warning. Validación: `python manage.py check` limpio en venv. Resultado esperado: pulsar `Estándar` ahora arranca el worker `vidstabdetect`+`vidstabtransform` escribiendo `stabilized_estandar.trf` y `stabilized_estandar_proxy.mp4` en `media-seq-ypb8co/`.
- 2026-04-21: Quick wins de performance del preview clásico (jitter de playback). (1) En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `applyCropFitCSS` y `applyCropFillCSS` ahora memoizan los últimos `transform`/`width`/`height`/`objectPosition` aplicados por elemento `<video>` en un `WeakMap` y solo llaman `setProperty()` cuando el valor cambia respecto al frame anterior. Durante reproducción con `crop keyframes` manuales esto recorta 3-5 escrituras CSS redundantes por tick (~60Hz) que invalidaban estilo computado y compositing aunque el valor real no se moviera. (2) En [frontend/src/styles/app.css](frontend/src/styles/app.css) `.workflow-preview-backdrop-video` pasó de `filter: blur(28px) saturate(0.9); transform: scale(1.12)` a `filter: blur(20px) saturate(0.9); transform: scale(1.08)` y se le añadió `contain: paint; will-change: filter`. `blur(28px)` a fullscreen era el filtro más caro del stage del preview; el nuevo radio es aprox. la mitad de costoso de rasterizar por frame y la contención aísla sus repaints de los `<video>` primarios y subtítulos. Validación: build de Vite limpio (`dist/assets/index-*.js` 980 kB, sin warnings nuevos), sin errores en archivos editados.
- 2026-04-21: Se blindó el enrutamiento de media por secuencia para que master/proxy y estabilización escriban siempre dentro del `media_key` correcto de la secuencia objetivo (caso reproducido en `editorial-approved-1775804275716-thematic-25-42962` → `media-seq-ypb8co`). Causas: el frontend caía a `activeDerivedSequenceBlocks` cuando la secuencia objetivo no había poblado aún sus bloques, y el backend aceptaba `sequence_blocks` y `media_key` del payload sin validarlos contra el draft, por lo que un cambio rápido de secuencia podía renderizar master/proxy con rangos de otra secuencia o colocar los `.trf`/`stabilized_*_proxy.mp4` en carpeta ajena. Cambios: (1) en [backend/apps/editor/views.py](backend/apps/editor/views.py) `ProjectSequenceMediaPreparationView.post` resuelve el snapshot del draft para `sequence_id`, compara firmas con `_build_sequence_source_signature` y, si hay mismatch, usa los bloques del draft y autocorrige `media_key`; (2) en el mismo archivo `ProjectSequenceStabilizationView.post` normaliza `media_key` del payload contra `_resolve_sequence_snapshot_media_key(snapshot)` y reencamina `master_relative_name`/`proxy_relative_name` si apuntan a otra carpeta; (3) `_start_sequence_stabilization_worker` valida que el `source_path` resuelto esté físicamente dentro del `media_key` canónico y, si existe `master.mp4` en la carpeta correcta, redirige; si no, registra un warning estructurado. (4) en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleTriggerSequenceClipPreparation(...)` deja de caer a `activeDerivedSequenceBlocks`: si la secuencia objetivo no tiene bloques propios aborta con error explícito. Validación: `python manage.py check` limpio con el venv del proyecto y ESLint sin errores nuevos en `DashboardPage.jsx` (solo persisten 25 pre-existentes en líneas >21000).
- 2026-04-19: Se corrigió el modal de crop/reframe para que muestre el clip correcto al abrir con proxy de rango. El proxy de rango tiene los clips en posiciones absolutas dentro del archivo (p. ej. clip 0 en 43.920s–46.800s), pero la lógica del modal usaba `sequence_start_ms/sequence_end_ms` (0–2880ms) como coordenadas del video, causando que el seek inicial fuera a 0s y mostrara el clip equivocado, y que "Clip 57.95s" se mostrara en la barra de keyframes. Cambios: (1) En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) se añadió prop `sequencePreviewRangeStartMs`; `resolveTimelineBlockStartMs/EndMs` ahora devuelven `source_in_ms - rangeStartMs` / `source_out_ms - rangeStartMs` en vez de `sequence_start_ms/sequence_end_ms`; el sort de `sequenceBlocks` usa posición proxy; `sequenceDurationSec` toma el máximo de todos los bloques en vez del último. (2) En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) el seek inicial al abrir el modal corrige `targetSeconds = (previewSourceMs - rangeStartMs) / 1000` en vez de `previewSourceMs / 1000`. (3) En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se pasan los nuevos props `sequencePreviewRangeStartMs` y `cropSequencePreviewRangeStartMs` desde `previewSequenceMediaPreparationState.rangeStartMs`. Resultado: el modal de crop posiciona el video en la posición proxy correcta del clip seleccionado, los keyframes manuales se resuelven bien (son tiempo local al clip, no cambian), y la barra de timeline muestra la duración real del clip.
- 2026-04-18: Se corrigió una causa raíz de `no reproduce` en el preview clásico que no venía del video ni del crop, sino del propio ciclo de vida de `DashboardPage`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `useClassicPreviewTransport(...)` recibía `getActiveBlock: () => activeBlock` antes de que `activeBlock` estuviera inicializado en el archivo; durante cleanup/HMR el transporte podía llamar `stopPlayback()` y disparar `Cannot access 'activeBlock' before initialization`, dejando la página en error boundary y los `<video>` del preview en estado inválido (`FFmpegDemuxer: demuxer seek failed`). Se sustituyó ese cierre por `activeBlockRef.current`, actualizado después de resolver el bloque activo real. Resultado verificado en la página activa: la secuencia vuelve a hidratarse, el error visible desaparece y el preview avanza de `0` a `2.09s` en una prueba de `2.5s` sobre B1.
- 2026-04-18: Se corrigió además una fuga de estado en el transporte del preview clásico al llegar a EOF. La instrumentación en la página activa mostró un caso reproducible donde el video visible quedaba `ended: true` pero el playerbar seguía en modo `Pause`, dejando la siguiente reproducción en un estado raro hasta volver a cuear un clip. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) se añadió un listener de `ended` sobre los dos slots primarios: si termina el video activo mientras el transporte aún cree que está en `playing`, ahora fuerza la salida correcta a `idle` y fija el playhead al final de la secuencia; si `repeat` está activo, reinicia el playback en vez de quedar colgado. Resultado buscado: que EOF no deje el preview en un estado imposible ni contamine la siguiente reproducción.
- 2026-04-18: Se confirmó y corrigió la causa restante del glitch visual en el preview clásico con `crop keyframes` manuales. La traza runtime sobre B1 mostró que el video seguía presentando frames de forma continua, pero el loop que actualizaba el crop dependía de un reloj/cadencia demasiado lenta: en este entorno `requestAnimationFrame` y `requestVideoFrameCallback` iban a unas pocas veces por segundo mientras el `<video>.currentTime` seguía avanzando. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el loop imperativo del preview dejó de resolver el tiempo del crop desde `currentSequenceTimeMs` y ahora toma el `currentTime` real del video activo (`sequence-original` cuando está disponible, con fallback seguro). En ese mismo archivo el scheduler del loop pasó de `requestAnimationFrame` a un timer corto (`16ms`) y `applyClipStylesImperatively(...)` volvió a sembrar inmediatamente `autoFrameTrackingLastStyleRef` para que React no limpie el estilo justo al arrancar playback. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) además el slot primario oculto dejó de recibir `videoStyle` React mientras el preview está en modo imperativo, evitando que el staging quede adelantado respecto al visible. Resultado verificado: el slot visible ya no arranca en blanco ni persigue al staging, y la traza de B1 pasó de cambios grandes y espaciados a actualizaciones frecuentes mientras avanza entre keyframes.
- 2026-04-18: Se corrigió la causa restante de la sensación de "resistencia" en `crop keyframes` manuales. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) `resolveCropRectAtTime(...)` volvió a interpolar linealmente entre keyframes, quitando la curva suave sensible a distancia que se había introducido el 2026-04-17. Hallazgo de runtime: en la secuencia `editorial-approved-1775801543726-thematic-14-67066`, el preview sí estaba obedeciendo los keyframes persistidos, pero esa curva no lineal alteraba la traducción temporal esperada y hacía que el movimiento se sintiera como si el preview se resistiera a las coordenadas manuales. Resultado buscado: que preview principal y modal traduzcan los keyframes exactamente en el tiempo marcado por edición manual, sin desaceleraciones artificiales en cada tramo.
- 2026-04-18: Se corrigió además un desfase en el arranque imperativo del preview clásico. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `applyClipStylesImperatively(...)` dejó de resolver el `cropRect` manual siempre en `time=0` del bloque y ahora acepta el `sequenceMs` real para traducirlo a `blockLocalTimeMs`. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) playback start y auto-advance ya le pasan ese tiempo real (`startSequenceMs` / `nextBlockSequenceMs`). Resultado buscado: si el usuario entra a reproducir a mitad del clip o el handoff continúa dentro del siguiente bloque, el primer frame visible ya nace con el encuadre manual correcto en vez de “volver” momentáneamente al crop del inicio del clip.
- 2026-04-18: Se corrigió la sensación de "resistencia" del preview clásico frente a tracking manual con keyframes. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el slot visible deja de recibir el `style` React base del frame cuando el preview entra en modo imperativo, igual que ya pasaba con el `<video>`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `buildPreviewPlaybackTrackStyle(...)` pasó a escribir siempre tanto `videoStyle` como `frameStyle` para `fit` y `fill`. Resultado: durante playback manual con keyframes, React ya no empuja el frame hacia un estado throttled mientras el loop RAF intenta corregirlo, así que desaparece el efecto de reacomodo continuo.
- 2026-04-18: Se endureció la autoridad del preview principal durante playback para eliminar conflictos residuales alrededor del arranque del movimiento manual. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `previewPrimaryImperativeVideoStyleActive` dejó de activarse solo cuando había trayectoria/manual detectada y pasó a cubrir toda la reproducción no-split; en paralelo, el loop RAF del preview ya no aplica solo correcciones parciales sino el estilo completo del clip (`videoStyle + frameStyle`) también cuando no hay trayectoria, cuando el tracking está desactivado o cuando la muestra no devuelve cámara. Resultado buscado: el preview visible deja de alternar entre “React base” e “imperativo correctivo” según el estado interno del bloque y pasa a tener un único escritor durante todo el playback.
- 2026-04-18: Se corrigió otra fuente de reacomodo dentro del modal de Crop. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) la sincronización inicial del video del modal dejó de ejecutarse cada vez que cambiaban dependencias del preview padre: ahora solo vuelve a seekear al abrir el modal o si cambia realmente la fuente de media usada por Crop. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) además se eliminó el segundo seek redundante a `clipStartSec` que competía con esa sincronización. Resultado: el modal ya no debería reinyectar `currentTime`/pause mientras reproduces dentro de Crop.
- 2026-04-18: Se corrigió el arranque del modal de crop para que abrirlo no relance innecesariamente la preparación de media de secuencia cuando el modal ya tiene una URL continua usable. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) `handleOpenCropModal(...)` dejó de mirar `sequencePreviewUsesPreparedMedia/sequencePreviewVideoUrl` del preview principal y ahora decide con `cropSequencePreviewUsesPreparedMedia/cropSequencePreviewVideoUrl`. Resultado: el modal ya no debería disparar otra vez la fabricación de master/proxy sólo por abrir Crop, y las dos barras de progreso de media en el preview clásico dejan de reactivarse por ese motivo.
- 2026-04-18: Se corrigió el último desajuste del modal de crop cuando ya existía una URL reproducible para la secuencia pero la UI seguía mostrando el placeholder de `Preparando secuencia...`. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) `previewCropModalPreparingSequence` dejó de depender de `sequencePreviewUsesPreparedMedia/sequencePreviewVideoUrl` del preview principal y ahora usa la media específica de Crop (`cropSequencePreviewUsesPreparedMedia/cropSequencePreviewVideoUrl`). Resultado: si Crop ya puede renderizar la secuencia continua aunque el preview principal siga tratando esa media como `stale`, el modal deja de tapar el video con el placeholder y sigue permitiendo que la regeneración avance en segundo plano.
- 2026-04-18: Se corrigió el atasco del modal de crop cuando el backend todavía devolvía `sequence-media-preparation = ready` pero el frontend ya había marcado esa media como `stale` por cambio de firma. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el callback `onRequirePreparedSequencePreview` dejó de aceptar cualquier `ready` restaurado como válido: ahora compara `sourceSignature/source_signature` contra la firma actual de la secuencia y, si ese `ready` ya está vencido, relanza `handleTriggerSequenceClipPreparation(...)` en vez de dejar el modal clavado en `Preparando secuencia...` sin video. Resultado buscado: al abrir `Crop`, una media preparada vieja ya no bloquea el render del video; se regenera automáticamente y el modal puede salir del placeholder.
- 2026-04-18: Se corrigió la semántica de apertura del modal de crop para que deje de trabajar sobre el master/source cuando falta media preparada. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) `handleOpenCropModal(...)` ahora exige `sequencePreviewUsesPreparedMedia`; si la secuencia todavía no tiene media continua, dispara la preparación y deja el modal pendiente hasta que el backend devuelva `ready`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) además el crop workspace pasa a usar `previewEditingBlock` como clip seleccionado estable y se añadió un callback que sincroniza o lanza `sequence-media-preparation` antes de abrir Crop. Resultado buscado: el modal de crop vuelve a representar la secuencia editada, no el video madre completo.
- 2026-04-17: Se corrigió una regresión del modal de crop donde podían desaparecer el clip activo, sus keyframes manuales y la tira de clips al abrir el modal justo cuando `previewLayoutBlock` quedaba nulo o transitorio. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se separó el concepto de bloque de playback/layout del bloque de edición: el crop modal, la resolución de `selectedClipDisplayMode`, los ajustes manuales (`selectedClipAdjustment`) y los ignored samples ahora usan `previewEditingBlock = previewLayoutBlock ?? selectedTimelineBlock ?? activeBlock`. Con esto el modal sigue anclado al clip seleccionado aunque el bloque presentado del preview cambie o se quede momentáneamente vacío, y vuelven a aparecer `clipLabel`, `selectedBlockId` y los `cropKeyframes` del clip correcto.
- 2026-04-17: Quedaba un temblor residual en el preview principal incluso después de quitar los escritores de estilo en conflicto y el throttle grueso del tracking. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) añadí una amortiguación ligera dentro del propio loop de playback: el preview ya sigue frame a frame, pero ahora interpola con un `alpha` moderado hacia la trayectoria objetivo y hace `snap` cuando la diferencia cae por debajo de un epsilon pequeño. El objetivo no es volver a meter inercia artificial, sino filtrar micro-jitter de la trayectoria aplicada para que el encuadre deje de vibrar mientras sigue respondiendo rápido.
- 2026-04-17: El preview principal todavía se sentía "resistido" frente al movimiento aunque ya no hubiera dos escritores peleándose por el mismo estilo. La causa restante estaba en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): el tracking live del preview seguía un esquema `throttle cada 280ms + transition CSS 300/500ms`, mientras el modal de crop ya resolvía por `requestAnimationFrame` con `transition: none`. Se alineó el preview principal con ese enfoque más directo: se quitó el throttle temporal, se eliminaron las transiciones CSS del tracking live y el update ahora sigue la trayectoria frame a frame, saltándose solo deltas minúsculos. Objetivo: quitar la sensación de inercia artificial o de que el encuadre "se resiste" antes de acompañar al sujeto.
- 2026-04-17: Intensifiqué otra vez los tres niveles de estabilización, pero manteniendo una separación clara entre usos. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) `Suave` pasó a un cleanup más útil sin dejar de ser discreto (`stepsize=4`, `mincontrast=0.13`, `smoothing=60`, `zoom=3.5`, `maxshift=160`, `maxangle=0.12`), `Estándar` subió a una corrección editorial más seria (`accuracy=15`, `stepsize=2`, `mincontrast=0.08`, `smoothing=140`, `zoom=9.5`, `maxshift=320`, `maxangle=0.22`) y `Intenso` ganó todavía más autoridad para golpes bruscos (`mincontrast=0.025`, `detect_width=1600`, `smoothing=320`, `zoom=21`, `zoomspeed=0.14`, `maxshift=760`, `maxangle=0.42`). La intención fue empujar los tres escalones hacia arriba sin que el zoom adaptativo empiece a perseguir microcambios de forma nerviosa. Requiere rerun del modo activo para regenerar proxy y `.trf`.
- 2026-04-17: Volví a endurecer la familia completa de presets de estabilización para la secuencia abierta, ya con una escalera más separada entre `Suave`, `Estándar` e `Intenso`. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) `Suave` subió a una corrección editorial visible pero todavía contenida (`smoothing=42`, `zoom=2.5`, `maxshift=128`, `maxangle=0.10`), `Estándar` pasó a un punto medio ya bastante serio (`smoothing=96`, `zoom=7.5`, `maxshift=256`, `maxangle=0.18`) y `Intenso` quedó aún más dominante sobre golpes abruptos (`mincontrast=0.03`, `detect_width=1440`, `smoothing=260`, `zoom=18`, `zoomspeed=0.16`, `maxshift=640`, `maxangle=0.38`). La intención es que el preset fuerte absorba bastante más trayectoria impredecible sin que el zoom adaptativo empiece a perseguir nervioso cada microvariación. Requiere rerun de estabilización para reflejarse en el proxy y el `.trf`.
- 2026-04-17: Ajusté `Intenso` una vez más buscando una sensación más suave y menos “zoom que persigue”. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) el preset pasó a `stepsize=1`, `detect_width=1280`, `smoothing=180`, `zoom=14`, `maxshift=480` y `maxangle=0.30`, pero con `zoomspeed=0.22` para que el zoom dinámico no bombee tan nervioso entre frames. La idea es darle mucha más autoridad a la trayectoria suavizada sin volver loca la escala adaptativa. Requiere rerun de `Intenso` para evaluar el cambio.
- 2026-04-17: Reforcé otra vez el preset `Intenso` porque visualmente seguía sintiéndose demasiado cercano al original. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) `Intenso` pasó a una detección más fina (`stepsize=2`, `mincontrast=0.05`) y a una búsqueda sobre frames más grandes (`detect_width=960` en vez de 640), para no quedarse sólo con una corrección gruesa seguida de zoom. Además subieron bastante `smoothing`, `zoom`, `zoomspeed`, `maxshift` y `maxangle`, con el objetivo específico de comerse mejor golpes abruptos de live usando traslación, rotación y escala reales. Esto requiere rerun del modo para que el nuevo `.trf` y el proxy estabilizado reflejen la detección mejorada.
- 2026-04-17: Endurecí los presets de estabilización de secuencia para material tipo live multicámara con golpes bruscos. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) `Suave`, `Estándar` e `Intenso` quedaron alineados entre preview y export final: ahora usan más `smoothing`, `optalgo=gauss`, límites explícitos de `maxshift` y `maxangle`, y `Intenso` pasó a `optzoom=2` con `zoomspeed` más alto para absorber mejor movimientos súbitos mediante traslación, rotación y escala, no sólo con zoom fijo. Esto requiere rerun de estabilización para regenerar el proxy y el `.trf` con los nuevos settings.
- 2026-04-17: Se corrigió una regresión en los modos de estabilización del preview clásico (`Suave`, `Estándar`, `Intenso`). El backend actual entrega preview estabilizado sólo por `proxy_url`, pero [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) seguía normalizando únicamente `output_url`, así que el botón podía verse `ready` aunque el preview siguiera reproduciendo `proxy.mp4` sin zoom/estabilización real. El normalizador ahora conserva `proxyUrl` y `proxyRelativeName`, los persiste en `sequenceStabilizationModes` y vuelve a habilitar el cambio real a `stabilized_<modo>_proxy.mp4`.
- 2026-04-17: La barra espaciadora volvió a controlar `play/pause` dentro del modal de crop. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal ahora escucha `Space` en captura mientras está abierto, evita que la tecla haga scroll o quede absorbida por foco residual de botones, y respeta solo las exclusiones mínimas de campos editables (`input`, `textarea`, `select`). Si el modal estaba en reproducción por clip, `Space` sigue ese mismo scope; si no, alterna la reproducción de secuencia como en el preview principal.
- 2026-04-17: El modal de crop ahora sigue el clip que realmente está corriendo cuando reproduces la secuencia dentro del propio modal. El video del modal ya actualiza el bloque activo del workspace al cruzar cortes, así que el header, los keyframes manuales y el estado `manual/auto` saltan al clip en curso sin forzar seek del preview principal.
- 2026-04-17: Eliminé una sincronización redundante entre `previewLayoutBlock` y la selección React del timeline. El cambio de clip durante playback sigue viniendo del transporte clásico en los cortes reales, mientras el workspace de crop continúa leyendo el bloque activo del preview. Con esto se evita meter churn extra de estado sin añadir seeks nuevos al playback.
- 2026-04-17: Se sincronizó el clip activo del editor con el playhead durante playback para que el crop box, los keyframes y las ediciones manuales salten automáticamente al bloque que entra en frame. El workspace de crop ya no depende de una selección manual congelada cuando la reproducción cruza cortes.
- 2026-04-17: Se corrigió el playback del preview principal para que los `crop keyframes` manuales se apliquen en tiempo real durante reproducción, incluso en clips portrait cortos donde antes el auto frame tenía un loop imperativo pero el crop manual dependía del estado React throttled del playhead. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el loop imperativo del preview ahora resuelve el ajuste manual del bloque actual (`baseCropRect + cropKeyframes`) en cada frame cuando existe override manual efectivo, y solo cae al tracking automático cuando el clip no tiene override manual. Resultado observado en la secuencia `editorial-approved-1775801543726-thematic-14-67066`: el preview principal vuelve a mover `left/top` en clips `fit` manuales y `object-position` en clips `fill` manuales durante playback. Validación: `get_errors` limpio, `npm run build` correcto en `frontend` y verificación en navegador sobre clips manuales `portrait` de la secuencia abierta.
- 2026-04-17: crop modal compactado para entrar sin scroll en viewport normal, con preview reducido y rails/metadata mas densos. El modal ahora separa `Reproducir secuencia` de `Reproducir solo clip`, y el play de clip corta al llegar al final del bloque aunque el proxy siga siendo continuo. Tambien se corrigio la persistencia del ajuste manual para no guardar `baseCropRect` derivado y la precedencia manual fuerza override inmediato cuando existen `cropKeyframes`.
- 2026-04-17: Se corrigió la precedencia entre crop manual y auto frame para que el preview principal deje de seguir la trayectoria automática cuando el clip tenga un override manual efectivo, especialmente al crear `crop keyframes` en un clip que ya tenía auto frame. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el tracking del preview ahora compara el ajuste actual del clip contra el ajuste devuelto por el motor para el aspecto activo, ignorando solo `ignoredAutoFrameSampleTimes`. Si el clip tiene keyframes manuales o cualquier diferencia real frente al ajuste del motor, el preview ya no vuelve a inyectar el auto frame encima; si se borran todos los keyframes y el clip vuelve a coincidir con el ajuste del motor, el auto frame se reactiva sin depender de un flag histórico. En el modal de crop, además, se añadió borrado de muestras automáticas (`ignoredAutoFrameSampleTimes`) para tener más libertad al depurar el seguimiento del motor, y se compactó la UI: el transporte principal pasó al centro bajo el video, se redujo el texto accesorio y se apretó el espaciado vertical entre la línea manual y la automática. Validación: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: Se corrigió una incoherencia entre el playback del modal de crop y el preview principal. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el `crop box` del modal ya no depende solo de `onTimeUpdate` del `<video>` ni de una transición CSS adicional sobre `left/top/width/height`. Ahora, mientras el video del modal está reproduciendo, `videoCurrentTime` se refresca por `requestAnimationFrame` y el rectángulo se resuelve sin tween CSS extra (`transition: none`). Esto evita el doble suavizado y los saltos por baja frecuencia de `timeupdate`, acercando el movimiento del crop en el modal al que ya usa el preview principal, que resuelve el encuadre directamente desde el tiempo actual. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: La interpolación entre `crop keyframes` dejó de ser lineal y pasó a una curva suave sensible a distancia. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) `resolveCropRectAtTime(...)` ahora usa una base `smootherstep` para garantizar velocidad cero al inicio y al final de cada tramo, y encima aplica un refuerzo simétrico en la zona media cuando el salto de encuadre entre dos keyframes es grande. Resultado buscado: para movimientos cortos el encuadre sigue viéndose estable y consistente; para saltos largos, el crop acelera más en la mitad del trayecto y vuelve a desacelerar hasta llegar con velocidad cero al siguiente keyframe, dando una sensación más suave pero también más atenta cuando hay que recorrer mucha distancia. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: El modal de crop dejó de tratar los keyframes como botones de formulario y pasó a usar una mini barra de transporte temporal. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) los controles `anterior/siguiente/nuevo/borrar keyframe` se movieron junto al rail temporal y quedaron como botones compactos tipo playerbar. En ese mismo componente se añadió scrub real sobre las líneas de tiempo del modal (`clip range`, rail de keyframes y rail de muestras): hacer click o drag sobre esos rails ahora busca el video directamente en ese punto de la secuencia. Además se cerró la interferencia visual entre auto frame y crop manual: cuando el bloque ya está en `manual override`, el rectángulo de crop del modal deja de seguir la trayectoria suavizada de auto frame y usa solo `baseCropRect + cropKeyframes` manuales. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron los estilos del transporte temporal y el affordance visual de scrub. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: Se alineó la semántica temporal del modal de crop para que los rails inferiores usen la misma escala de toda la secuencia que la barra superior. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) los `crop keyframes` ya no se posicionan contra el ancho del clip aislado sino contra `sequenceDuration`, y las muestras de auto frame (`sample dots`) también quedaron distribuidas por tiempo real de secuencia sobre todo el ancho. Además el rail de keyframes y el rail de muestras ahora resaltan el span del clip seleccionado dentro de esa escala global. En [frontend/src/styles/app.css](frontend/src/styles/app.css) los botones `Keyframe anterior/siguiente`, `Nuevo keyframe` y `Borrar keyframe` pasaron a usar una variante compacta con el lenguaje visual del playerbar clásico, en lugar de quedarse como botones de formulario genéricos. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: Se corrigió la primera implementación de keyframes manuales de crop porque el drag estaba naciendo desde un rect desfasado respecto al tiempo real del modal. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) `handleCropRectPointerDown(...)` y `handleCropResizePointerDown(...)` ahora reciben el rect activo del modal, así que mover o redimensionar un keyframe ya no arranca desde el estado del preview principal sino desde el keyframe/rect interpolado realmente visible. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) `normalizeCropKeyframes(...)` además colapsa duplicados en el mismo instante, y crear un keyframe cerca de otro existente lo reemplaza en vez de apilar dos puntos casi iguales. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal ganó navegación `Keyframe anterior/siguiente`, seek directo al keyframe elegido y una etiqueta explícita `manual override` para dejar claro que, si el bloque ya fue tocado manualmente después de auto frame, ese ajuste manual sigue imponiéndose salvo que el usuario fuerce un overwrite de auto frame. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-17: Se añadió una primera ruta real de crop manual keyframeado dentro del modal de crop. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) quedaron helpers puros para normalizar `cropKeyframes` e interpolar el `cropRect` activo por tiempo dentro del clip. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el preview activo ya resuelve `baseCropRect + cropKeyframes` del bloque seleccionado y expone al modal conteo por clip y limpieza de keyframes por bloque. En [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) mover o redimensionar el crop puede editar el keyframe seleccionado en vez del `cropRect` estático, además de crear/borrar keyframes manuales. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal ahora deja navegar por clips desde la barra de secuencia, hacer pulsación larga sobre un clip para abrir la acción `Borrar keyframes`, crear keyframes en el tiempo actual, seleccionarlos desde un rail dedicado y ver interpolación lineal del crop entre puntos. Validación final: `get_errors` limpio en frontend.

- 2026-04-16: Se limpió el chrome inferior del preview clásico para dejar de pelear contra un bloque diagnóstico sin función editorial en esa vista. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el botón de volumen dejó de vivir como clúster lateral separado y pasó a integrarse dentro del grupo central de transporte, así que vuelve a alinearse y centrarse como un control más del playerbar. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se eliminó también el ancho fijo especial del botón de volumen para que herede exactamente la misma caja que el resto de botones compactos. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el bloque `Más opciones` del classic editor quedó anulado por completo: estaba insertando un `details` grande debajo del preview con diagnóstico técnico y acciones redundantes, desperdiciaba espacio vertical y descomponía el CSS logrado del editor. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`; la comprobación visual en el navegador integrado siguió parcialmente limitada por la rehidratación inestable de la ruta `?project=25&sequence=...`, que a veces vuelve a mostrar la shell de librería en lugar de la secuencia activa.

- Playerbar responsive por prioridad: en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el transporte del preview quedó reagrupado en clústeres de volumen, transporte principal y acciones secundarias. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el playerbar ahora usa `container-type: inline-size` y degrada por prioridad cuando se achica el espacio: primero reduce botones, luego oculta navegación secundaria (`clip anterior/siguiente`, `repeat`), después elimina saltos de 10 frames y corta utilidades como `cut`, mientras el ruler superior sigue ocupando el 100% del ancho. El control de volumen además cambia a una variante compacta/vertical en anchos menores y, en estado extremo, el timecode sube a una fila propia encima del transporte principal.
- Eliminar clip y headers compactos en una línea: en [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) `handleDeleteSelectedTimelineBlock(...)` ahora no solo apaga todas las palabras del bloque eliminado sino que también las agrega a `subtitleHiddenWordIds`, para que el clip quede efectivamente fuera del transcript y de los subtítulos. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el editor clásico compacta mejor sus headers: el transcript deja de envolver tabs y ayuda a dos líneas, oculta el título del módulo cuando el panel se estrecha, y el timeline oculta primero el nombre del clip y luego los botones de merge para conservar siempre visibles `Timeline` y el control `Zoom` con su slider en una sola línea.
- Restauración de títulos de módulos y sync del header: en [frontend/src/styles/app.css](frontend/src/styles/app.css) los `h2` de los paneles clásicos de transcript y preview dejaron de estar forzados a `display: none`, así que los títulos de módulo vuelven a mostrarse. En [frontend/src/App.jsx](frontend/src/App.jsx) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadió un handshake explícito `workflow:request-chrome-state` / `workflow:chrome-state` para que el topbar superior arranque con el mismo estado colapsado/expandido que el editor, evitando el desajuste donde el botón de minimizar parecía no funcionar o el shell aparecía expandido aunque la vista interna seguía colapsada.
- Fix de colapso del preview portrait: en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el helper que calcula el `fit` explícito del `workflow-preview-stage` ahora parsea correctamente valores de `aspectRatio` en string como `9 / 16`. El bug estaba forzando un fallback de ratio casi nulo y comprimía el stage a ~5px de ancho en el editor clásico. Tras el fix, el stage vuelve a medirse con ancho real dentro del canvas disponible.
- Preview con fit explícito del stage: en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el canvas del preview clásico ahora mide su caja disponible y calcula un `width/height` explícitos para `workflow-preview-stage` usando el `aspectRatio` activo. Esto sustituye la vieja dependencia de `portrait = height: 100%`, que producía tamaños distintos entre Chrome/Edge o entre ventanas con diferente chrome vertical. Resultado esperado: el canvas usa siempre la misma lógica de `fit dentro del espacio disponible` y el preview debería variar mucho menos entre navegadores cuando cambia el alto útil.
- Header clásico más robusto entre navegadores: en [frontend/src/styles/app.css](frontend/src/styles/app.css) el header de transcript y preview del editor clásico dejó de depender del umbral de container query para ocultar títulos redundantes. Ahora esos `h2` del panel se ocultan siempre en el editor clásico, y en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el botón de salida del preview usa directamente la variante corta `Salir`. Objetivo: evitar diferencias de métricas entre Chrome/Edge o entre alturas útiles distintas, y sostener una sola línea estable en ambos headers.
- Headers compactos en una sola línea: en [frontend/src/styles/app.css](frontend/src/styles/app.css) el editor clásico ahora trata los paneles de transcript y preview como contenedores `inline-size` y fuerza sus headers a no partirse en dos filas. Cuando el panel de transcript se estrecha, el título `Transcript de la secuencia` se oculta para dejar solo tabs y ayuda; cuando el preview entra en compacto, también se oculta el título del panel y el botón `Salir al selector` pasa a una variante corta (`Salir`) para que acciones como `Revisado`, `Undo`, `Redo` y `Exportar` se mantengan en una sola línea. El ajuste de labels quedó cableado en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx).
- Preview por eje limitante: en [frontend/src/previewTitleLayout.js](frontend/src/previewTitleLayout.js) y [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el título y los subtítulos del preview dejaron de escalar ancho y tipografía por ejes distintos. Ahora todo el overlay usa una escala uniforme derivada del eje limitante del frame visible (`min(width/baseWidth, height/baseHeight)`), de modo que si el frame se vuelve demasiado angosto mandan los laterales y también cae la fuente/padding; si sobra ancho, manda la altura. Con esto el preview se adapta “por techo o por laterales” según el límite real del encuadre visible.
- Preview fiel al frame visible: en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el preview clásico ahora mide no solo el tamaño del frame activo sino también su offset real dentro de `workflow-preview-stage`, de modo que título y subtítulos se posicionan y escalan contra el rectángulo visible del video y no contra todo el stage. En [frontend/src/previewTitleLayout.js](frontend/src/previewTitleLayout.js) el título pasó a usar coordenadas absolutas derivadas de ese frame visible, y los subtítulos convierten sus `cqw/cqh` a píxeles usando el ancho/alto reales del frame activo. Resultado: cuando el video queda con bordes laterales estrechos o el frame visible no ocupa todo el stage, los overlays se ajustan a esos límites y el preview se acerca más al encuadre real del export.
- Overlays PNG en una sola pasada: en [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py) se añadió `render_combined_overlay_frames(...)` para capturar título + subtítulos en un único render headless RGBA, incluyendo huecos donde solo queda visible el título. [backend/apps/editor/services.py](backend/apps/editor/services.py) usa esta ruta cuando ambas capas salen por `headless_png`, generando un solo `overlay_combined_concat.txt` y eliminando una de las dos composiciones `overlay` en FFmpeg. Esto reduce memoria, trabajo de composición y el riesgo de desalineación entre título y subtítulos sin abandonar la fidelidad visual actual.
- Prueba real de export y detalle del modal: en [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal ahora muestra también `Encoder`, `Capas overlay`, `Pipeline overlay`, `Compose`, `Filtros video` y `Filtros overlay`, además del detalle de workers/PNGs en overlays. La validación HTTP real contra `project=25` confirmó dos cosas: la metadata nueva del modal llega viva al job (`overlay_layer_count`, `overlay_pipeline_summary`, backends CPU/NVENC y workers) y la ruta headless combinada ya corre dentro del worker real sin caer a fallback por contexto async. La corrección quedó en [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py): migración completa a `async_playwright` y envío de `progress_callback` vía `asyncio.to_thread(...)` para no tocar Django ORM desde el loop async del renderer.
- Compose final realmente en una sola capa: en [backend/apps/editor/services.py](backend/apps/editor/services.py) se eliminó el remate que todavía añadía un `ASS` de título después de `overlay_combined_concat.txt`. La prueba viva `probe_combined_overlay_http_v6` ya muestra `overlay_render_method = headless_png_combined`, `main_title_render_method = headless_png`, `subtitle_render_method = headless_png`, `overlay_layer_count = 1` y un `filter_complex` final con un único `overlay` sobre el video base, sin `ass=filename` adicional.
- Export final menos lento en render: en [backend/apps/editor/services.py](backend/apps/editor/services.py) el perfil NVENC `final` bajó de una combinación demasiado cara para este pipeline (`p5`, `cq 18`, `lookahead 16`) a un punto más pragmático (`p4`, `cq 19`, sin lookahead). En la misma pasada el filtro por clip deja de insertar `fps=` cuando el source ya viene al mismo frame rate objetivo, evitando remuestreo redundante durante crop/scale/blur. El cuello de botella real venía de combinar video recortado/escalado, overlays PNG full-frame y encode de alta calidad en una sola pasada; estos ajustes recortan trabajo sin cambiar el layout visual.
- Export final vuelve a 60 fps: en [backend/apps/editor/services.py](backend/apps/editor/services.py) el export de video ya no baja a `30 fps` cuando los subtítulos del preview están activos. Quedaba un clamp heredado en la ruta `final` que recortaba materiales `60 fps` aunque el encode y la captura de overlays ya estaban preparados para trabajar a la tasa real. Ahora `export_fps` conserva `source_export_fps` y el MP4 final vuelve a salir con la cadence original del material/editor.
- Export jobs colgados: el backend ahora autocorrige exports `pending` sin worker activo. En [backend/apps/editor/views.py](backend/apps/editor/views.py) se agregó limpieza de jobs stale por proyecto al consultar historial/estado o al iniciar nuevos exports; si un job quedó `pending` pero el worker de export ya no existe y pasaron varios minutos, pasa a `error` recuperable con mensaje explícito en vez de quedar congelado indefinidamente en la UI.
- Arranque backend en Windows: [start-backend.ps1](start-backend.ps1) dejó de usar el operador `??`, que estaba rompiendo en Windows PowerShell clásico durante el reinicio manual del backend. Ahora resuelve `CommandLine` y `VIDEO_EDITOR_BACKEND_AUTORELOAD` con checks compatibles para que el script vuelva a servir como restart local.
- Export rapido con subtitulos: en [backend/apps/editor/services.py](backend/apps/editor/services.py) el modo `fast` ya no baja forzosamente a `30 fps` cuando los subtitulos pueden ir por ASS, así que materiales portrait a `60 fps` conservan su cadence original tambien con overlays. En la misma pasada el preset rapido subió de `cq 22` a `cq 21` para recuperar un poco de calidad sin volver al costo del modo `final`.
- Titulo exportado vs preview: en [backend/apps/editor/services.py](backend/apps/editor/services.py) el titulo dejó de ir por `ASS` en modo rapido cuando los subtitulos usan `ass_fast`; ahora mantiene subtitulos por ASS pero vuelve a renderizar el titulo como PNG headless para conservar la caja, padding y posicion visual del preview. También se alineó el cálculo de ancho/tamaño base del titulo con el preview real (`88%` de ancho base y escala de fuente equivalente a `4.2cqh`).
- Modal de export y diferencia visual: en [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal ahora traduce `fast/final` a una lectura humana (`Perfil Fast` y `Perfil Fine`) y muestra por separado como salen `Titulo` y `Subtitulos`, incluyendo una alerta explícita cuando ambos usan renderers distintos. Esto permite detectar desde el modal cuándo hay riesgo real de diferencias de caja, padding o peso visual frente al preview.
- Exportar = estado del editor: en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el botón principal `Exportar` ya no fuerza `fast` para video. Tanto export individual como batch salen por `final`, que es la ruta orientada a reproducir el draft actual del editor. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) el modal deja de enfatizar `fast/fine` y pasa a mostrar `Estado del editor` y `Origen: Draft del editor`.
- Titulo consistente entre preview y navegador: en [frontend/src/previewTitleLayout.js](frontend/src/previewTitleLayout.js) el título del preview ahora usa la misma geometría proporcional del export, calculada desde el tamaño real del frame visible. [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) observan el tamaño del canvas con `ResizeObserver` y aplican ancho, font-size, padding, radio y sombra en píxeles derivados del frame, en lugar de depender de `cqh`/`rem`. Además el título del preview fija `Bahnschrift` como familia para acercarlo al render exportado.
- Progreso fino de overlays: en [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py) los renderizadores headless de título y subtítulos ahora emiten callbacks de progreso durante la captura. [backend/apps/editor/services.py](backend/apps/editor/services.py) persiste esos contadores en metadata (`overlay_detail_processed`, `overlay_detail_total`, `overlay_detail_rendered_pngs`) y [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) los muestra en el modal para que el avance ya no salte solo por secciones amplias.
- Calidad máxima y render paralelo de subtítulos: en [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py) la ruta PNG/headless de subtítulos ahora puede dividir el trabajo entre hasta 4 workers Chromium cuando hay suficiente carga, reportando hitos de 1% en el progreso fino. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el modo `final` sube la calidad NVENC (`preset p5`, `cq 18`, `lookahead 16`) y los subtítulos PNG ya no se recortan a `30 fps`, sino que capturan a la tasa real del export para priorizar fidelidad visual.
- Modal con progreso por worker: en [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py) cada worker paralelo de subtítulos reporta su estado (`queued`, `starting`, `running`, `done`) y sus contadores propios. [backend/apps/editor/services.py](backend/apps/editor/services.py) persiste ese snapshot en metadata y [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) ahora lo pinta como tarjetas por worker dentro del panel de overlays para que el avance paralelo se perciba visualmente y no se sienta estático.
- Export de video: el modo rapido ahora sale por defecto desde el frontend para MP4 y el backend evita el render headless PNG de subtitulos/titulo cuando existe ASS de overlay, usando `ass_fast` para reducir fuerte el tiempo previo al encode.
- Modal de export: la banda tecnica del bloque de overlays ahora queda separada del grid de subetapas, con contraste y label propios, para que los chips de metodo/render no queden semiescondidos durante el proceso.
- 2026-04-16: Se corrigió otra fuga de ancho útil en el timeline del editor clásico. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el panel del timeline dejó de forzar `min-width: 980px`, lo que antes impedía que el carril izquierdo aprovechara todo el ancho real disponible y obligaba a mantener scroll horizontal incluso al hacer zoom out. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) además el zoom mínimo del timeline ahora vuelve a seguir el ancho útil cuando el editor cambia de espacio horizontal, siempre que el usuario no haya fijado manualmente otro zoom; así el modo “ver toda la línea” se conserva al reacomodar transcript/preview sin perder perspectiva. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-16: Se corrigió el aprovechamiento del espacio en el editor clásico y un artefacto visual del timeline. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el editor clásico volvió a fijarse en layout lateral útil (`transcript` arriba/izquierda, `timeline` debajo de ese mismo carril y `preview` ocupando toda la columna derecha), eliminando el hueco muerto bajo el transcript que estaba desperdiciando altura de lectura. En [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) también se endureció el ciclo de vida del overlay de trim para que `Ajuste fino` no vuelva a quedar asomándose en el borde izquierdo si un drag se interrumpe; el overlay ahora nace fuera de pantalla y al cerrarse resetea `left/transform`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se compactó además el arranque vertical del timeline para pegar más el cabezal con el primer carril. Validación final: `get_errors` limpio, `npm run build` correcto y verificación visual en navegador con el editor usando la altura completa del transcript y sin badge fantasma de trim.

- 2026-04-16: Se añadió una marca persistente de cierre editorial por secuencia para dejar claro qué pieza ya quedó lista para exportar. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el header del editor clásico ahora muestra un botón `Revisado` antes de `Salir al selector`, y `Exportar` se movió al bloque junto a `Undo/Redo` para que el cierre final quede agrupado. Al marcar una secuencia, el estado se guarda en el draft como `reviewedForExport` / `reviewedForExportAt`, y en Slide 3 (`Secuencias creadas`) aparece una badge con check tanto en la lista compacta como en las cards grandes. En [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) esa metadata quedó normalizada para sobrevivir reloads y persistencia backend, y en [frontend/src/styles/app.css](frontend/src/styles/app.css) se sumó el tratamiento visual verde/check para que las secuencias revisadas se distingan de inmediato.

- 2026-04-16: Se rehízo el preview inline de Slide 4 (`Batch export`) para que se parezca al preview final del editor clásico en vez de quedar como un player mínimo clip-a-clip. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `buildBatchExportSequenceItems(...)` ahora entrega bloques reales de secuencia con `blockId`, timings, palabras activas, `clipDisplayModes`, `clipDisplayAdjustments`, posiciones de subtítulo/título y estado de `sequenceMedia`, lo que permite reconstruir la lógica visual del preview en el panel batch. En [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) el preview abierto dejó de renderizar solo un `<video>` plano: si la secuencia ya tiene `media preparada`, usa esa media continua para evitar pausas entre clips y, sobre ella, aplica el modo visual por bloque (`fit/fill/split`), blur de fondo para `fit`, subtítulos con el mismo registry del editor y el título principal con sus posiciones/escalas persistidas. Si todavía no existe media continua, mantiene fallback `clip a clip` pero ya con overlays y crop visual del bloque activo. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el shell batch se adaptó para alojar el mismo `workflow-preview-stage` del editor dentro de la card de Slide 4.

- 2026-04-16: Se refinó Slide 4 (`Batch export`) para que la grilla se vea pareja y el preview inline respete el aspecto elegido. En [frontend/src/styles/app.css](frontend/src/styles/app.css) las cards de `atelier-batch-export-*` ahora se estiran con una altura base común, el bloque principal interno quedó nivelado visualmente y el grid usa `auto-fit` con `align-items: stretch` para que los dos paneles se lean más ordenados. En [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) el preview abierto dejó de estar fijo a `16:9`: ahora recibe `selectedAspectPreset` desde Slide 4 y muestra el player inline con `16:9`, `9:16` o `1:1` según el botón activo. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-16: Se volvió más manipulable el ajuste del título principal y el resize de carriles del timeline. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el menú `Titulo principal` ahora expone sliders directos para `Tamaño` y `Ancho`, evitando depender solo del `long-press + rueda` sobre el overlay; además los grips laterales del título crecieron para que el ajuste manual no se sienta trabado. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) esos sliders quedaron cableados al mismo persistido por secuencia (`mainTitleFontScale` / `mainTitleWidthScale`). En [frontend/src/components/WorkflowTimelineTrackResizer.jsx](frontend/src/components/WorkflowTimelineTrackResizer.jsx), [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el resizer de `thumbnails` / `waveform` ganó hit area mayor, muestra el valor actual en px, acepta doble click para volver al valor base y el drag volvió a respetar todo el rango persistible (`video 42-180`, `audio 46-196`). Además la badge del trim dejó de decir `Exacto/Ajuste exacto` y pasó a `Ajuste fino`, para que el modo se lea como edición fina y no como un estado ambiguo. Validación previa de persistencia: `videoTrackHeight` y `audioTrackHeight` ya se estaban guardando en `localStorage` dentro de `video-editor:global-layout-preferences`, así que la preferencia se conserva al volver a entrar.

- 2026-04-16: Se corrigió un problema de foco pegado en el editor clásico que hacía que la barra espaciadora siguiera reabriendo controles ya usados en vez de volver a `Play/Pause`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `setSequenceKeyboardSurface("timeline")` ahora hace `blur()` explícito sobre controles interactivos activos (`input`, `select`, `button`, sliders/combobox) cuando el usuario vuelve al timeline, y en [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) el click sobre el canvas del preview también devuelve la superficie de teclado al timeline. Efecto validado en navegador: tras elegir aspecto o tocar el slider de volumen, un click en timeline deja de mantener esos controles capturando la barra espaciadora y `Space` vuelve a activar `Play/Pause`. En la misma pasada [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) renombró `Trim preciso` a `Ajuste exacto` para que el indicador de trim con modificador se entienda mejor. Validación final: `get_errors` limpio, `npm run build` correcto y smoke test real en la secuencia abierta.

- 2026-04-16: Se corrigió el caso real donde `Audio Fix` parecía no hacer nada al pulsar `Pendiente` aunque la UI tuviera una secuencia activa visible. La causa raíz en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) era doble: `handleRunSequenceAudioEnhancement(...)` ya podía resolver la secuencia activa desde refs vivas, pero `handleTriggerSequenceClipPreparation(...)` seguía exigiendo que la secuencia hidratada pasara `isPreparedMediaEligibleSequence(sequence)`. En esta ruta concreta la secuencia abierta en el editor llegaba sin `kind/status/origin`, así que la elegibilidad devolvía `false` aunque el timeline activo sí tuviera bloques válidos en `derivedBlocksRef`. Ahora, si la secuencia activa del editor coincide con el `sequenceId` pedido y existen bloques derivados válidos, el flujo permite disparar `sequence-media-preparation` y usa esos bloques vivos como fallback para construir `sequence_blocks`. Validación real en navegador sobre `project=25 / sequence=editorial-approved-1775353832995-thematic-7-50266`: el click pasó de solo hacer `GET ...status -> 404` a lanzar `POST /api/editor/projects/25/sequence-media-preparation/` con `202`, mostró `Media 6%`, completó `master/proxy`, arrancó automáticamente `Audio Fix` y terminó en `Fix ON`. Validación final adicional: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-16: Se endureció también el arranque de `Audio Fix` frente a cierres stale del render activo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleRunSequenceAudioEnhancement(...)` ya no depende solo del snapshot del render para `activeSequence` y `autoFrameSequenceBlocks`: si el editor visible conserva la secuencia en refs (`activeSequenceIdRef`, `sequenceDraftsRef`, `derivedBlocksRef`) pero el closure quedó desincronizado por rehidratación/HMR, el handler ahora recompone la secuencia y los bloques desde esas refs vivas y solo entonces decide si puede lanzar el flujo. Si aun así no encuentra una secuencia válida, deja un error explícito en status en vez de fallar en silencio. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`; la comprobación end-to-end en el navegador integrado siguió parcialmente bloqueada por la rehidratación inestable de la ruta `?project=25&sequence=...`, que al recargar vuelve a sacar la sesión del editor clásico.

- 2026-04-16: Se corrigió una regresión real del arranque de `Audio Fix` / `sequence-media-preparation` en el editor clásico. En [frontend/src/pages/DashboardPage.jsx](c:/AI_Projects/Video%20Editor/frontend/src/pages/DashboardPage.jsx) `handleTriggerSequenceClipPreparation(...)` seguía confiando en `previewSequenceMediaPreparationState`, que mezcla draft persistido con estado local; si una secuencia arrastraba `sequenceMedia.status = processing` desde una sesión vieja, el handler asumía que ya existía un job vivo y devolvía temprano sin volver a lanzar `POST /sequence-media-preparation/`. Ahora ese guard solo respeta corridas realmente vivas en memoria (`sequenceClipPreparationRun.status === processing`) y deja que el flujo vuelva a disparar la preparación cuando backend ya no tiene run activo. En paralelo, el pill de [frontend/src/components/ClassicEditorPreviewPanel.jsx](c:/AI_Projects/Video%20Editor/frontend/src/components/ClassicEditorPreviewPanel.jsx) dejó de mostrar progreso ficticio (`0%`) cuando `Audio Fix` está `stale` pero no hay media en curso: ahora diferencia `preparando media` con porcentaje real frente a `pendiente` sin job activo. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se implementó la aplicación de estabilización en el export final desde el master. En [backend/apps/editor/services.py](backend/apps/editor/services.py) se añadió `_resolve_stabilization_transforms_for_export(editor_draft)` que busca el `.trf` guardado durante la etapa de proxy y el modo activo (`sequencePreviewPlaybackMode`) en el draft. Si existe un `.trf` válido para el modo activo, `export_sequence_as_video(...)` inyecta un filtro `vidstabtransform` en el `filter_complex` de ffmpeg, aplicado clip a clip desde el master de la secuencia antes de pasar por el pipeline de crop/fit. El filtro usa exactamente los mismos parámetros (`zoom`, `smoothing`, `optzoom`) que el modo seleccionado. Si no hay `.trf` disponible el export sigue usando el master sin estabilización. Además `_resolve_sequence_stabilization_candidate` fue corregido para aceptar modo proxy-only (sin `output_relative_name`) usando `proxy_relative_name` como fallback. Flujo resultante: si el usuario tenía, por ejemplo, `Estándar` activo y hay un `.trf` guardado para esa secuencia, el export lo aplica en vivo sobre el master; si no, exporta master limpio.

- 2026-04-14: Se rediseñó el pipeline de estabilización de secuencia para seguir el principio master/proxy. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el worker `_start_sequence_stabilization_worker` ya no genera un video estabilizado de alta resolución (`stabilized_<modo>.mp4`); en cambio ahora aplica `vidstabtransform` directamente sobre el master a resolución de proxy (1280p, CRF 24) en un solo paso ffmpeg, guarda el archivo `.trf` de transforms de forma permanente para uso futuro al exportar, y finaliza con solo el proxy estabilizado (`stabilized_<modo>_proxy.mp4`). En `_synthesize_stabilization_ready_from_disk` la detección desde disco pasó a buscar el proxy en lugar del full-res. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `_normalize_sequence_stabilization_state` añadió `proxy_relative_name`, `proxy_url`, `transforms_relative_name`, `mode` y `mode_label`, y el flag `enabled` ahora se activa cuando hay proxy aunque el full-res sea vacío. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `stabilizationModeVideoUrls` usa solo `proxyUrl` (sin fallback a `outputUrl`), y `canUseStabilizedSequencePreview` queda en `false` cuando HD Draft está activo, garantizando que HD Draft siempre use el master original y Draft mode use los proxies estabilizados para cada modo. Flujo resultante: Draft = proxies (estabilizado si el estabilizador está activo, original en otro caso), HD Draft = master original, Export = master original (la aplicación de estabilización al export en vivo, usando el `.trf` guardado, queda pendiente como siguiente paso).

- 2026-04-15: Se corrigió un desfase de `Audio Fix` respecto al video del preview cuando el player no estaba usando media de secuencia. La causa era que el audio mejorado podía seguir activo aunque el video primario estuviera en fuente `original` multipart (timeline en tiempo fuente), mientras el sync del WAV usaba tiempo de secuencia. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ahora el audio mejorado solo se habilita/sincroniza si la fuente activa del slot primario es `sequence-original` o `sequence-stabilized`; en cualquier otro `kind` se pausa y se devuelve audio al video base. Resultado esperado: `Audio Fix` deja de entrar corrido en bloque/tiempo cuando el preview cae a fuente no secuencial.

- 2026-04-15: Se corrigió un desajuste de sincronía entre script/timeline y video cuando el preview usaba media de secuencia (`sequence-original` o `sequence-stabilized`). En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) `getSequencePreviewReferenceMs(...)` ya no depende primero de `currentSequenceTimeMs` (que podía quedar stale en cambios de modo/slot) y ahora mapea explícitamente el tiempo pedido del bloque (`sourceMs`) hacia tiempo de secuencia (`sequenceInMs + delta`) con clamps por rango. Efecto esperado: al saltar o reproducir desde un bloque del script, el preview arranca en el tiempo correcto del clip dentro de la secuencia y no vuelve al inicio del video por un playhead desactualizado.

- 2026-04-15: Se cambió la estrategia por defecto del export final para priorizar calidad de fuente. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `export_sequence_as_video(...)` dejó de preferir el estabilizado horneado y ahora resuelve el asset de export con `prefer_stabilized=False`, lo que prioriza `master` (o fuente original si no hay `master` listo). Objetivo: mantener `proxy/proxy estabilizado` como herramientas de preview rápido y reservar la compresión final para el último paso de entrega, evitando degradación acumulada por re-encode intermedio.

- 2026-04-15: Se habilitó uso real de CUDA/NVENC en la estabilización de secuencia (`Suave/Estándar/Intenso`) para equipos NVIDIA como MSI GS66 RTX 3070/3070 Ti. En [backend/apps/editor/views.py](backend/apps/editor/views.py) la etapa de transform ahora intenta `h264_nvenc` primero y mantiene fallback a CPU solo si falla; además se corrigió la configuración NVENC para evitar fallback silencioso por GOP inválido (ahora usa GOP compatible, CFR, `bf=0`, `keyint_min`, `sc_threshold=0` y reset de audio con `asetpts/aresample`). Validación real en ejecución: estado de corrida mostrando `Renderizando estabilizado (Intenso) con CUDA...` y archivo final [backend/media/timeline/project_25/sequence_media/media-seq-gjrk7i/stabilized_intenso.mp4](backend/media/timeline/project_25/sequence_media/media-seq-gjrk7i/stabilized_intenso.mp4) con `encoder = h264_nvenc` en `ffprobe`.

- 2026-04-15: Se corrigió un hueco de reutilización stale en `sequence stabilization` que podía exponer un `stabilized_intenso.mp4` de otra versión de secuencia cuando faltaba `source_signature` en la media base. En [backend/apps/editor/views.py](backend/apps/editor/views.py) ahora, si falta firma, se deriva desde `sourceClips/source_clips/blocks` para `media-preparation`, `stabilization` y `POST /sequence-stabilization`; además la ruta de reuse desde disco dejó de aceptar `ready` cuando existe `source_signature` solicitado pero el archivo sintetizado no trae firma coincidente. Resultado esperado: no volver a mostrar un estabilizado viejo como válido tras cambios de secuencia o rehidrataciones.

- 2026-04-15: Se corrigió la generación del proxy de estabilización de secuencia para evitar que `stabilized_intenso_proxy.mp4` arranque visualmente en un frame distinto al master estabilizado. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el comando ffmpeg del proxy ahora resetea timeline (`setpts=PTS-STARTPTS`), resetea audio desde cero (`aresample=async=1:first_pts=0`) y fuerza encode sin B-frames ni cortes de escena (`keyint=1:min-keyint=1:bframes=0:no-scenecut=1`) para mejorar consistencia de seek/playback en preview.

- 2026-04-15: Se corrigió el crash del preview clásico al recargar mientras existía estado de estabilización de secuencia y se añadió rehidratación real del proceso desde backend. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) se eliminó la referencia fuera de scope que disparaba `ReferenceError: modes is not defined` dentro del strip de estabilización. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la apertura del editor dejó de restaurar solo modos `ready`: ahora consulta siempre `sequence-stabilization-status`, recupera también modos `processing`, reconstituye el control activo de cancelación y reanuda el polling hasta que la corrida termine. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y comprobación real en navegador de que la recarga ya no deja la app en error boundary y vuelve a mostrar el estado persistido de estabilización sin exigir otro click manual.

- 2026-04-14: Se refinó de nuevo el strip de estabilización del preview clásico para quitar duplicación visual del modo activo durante procesamiento. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el chip lateral deja de repetir el nombre del modo (`Intenso 38%`) y pasa a una lectura genérica `Processing 38%`, manteniendo el modo seleccionado solo en su botón. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se ajustaron tipografía, padding y radios del strip para alinearlo con el chrome denso del editor clásico, con esquinas casi rectas y misma familia mono/escala que los demás controles pequeños. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se corrigió el hueco de UX del toolbar de estabilización donde pulsar `Suave/Estándar/Intenso` podía no dejar ninguna señal visible mientras todavía faltaba generar `master + proxy`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la preparación de media ahora deja un estado optimista inmediato, persiste error visible si el backend no puede arrancar la corrida y conserva estados `queued/stale` en la normalización de estabilización en vez de degradarlos silenciosamente a `idle`. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el modo seleccionado pendiente queda marcado visualmente y el toolbar muestra inline el detalle de preparación (`master/proxy`, mensaje y porcentaje) en lugar de depender solo del tooltip. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron los estilos del estado `queued` y de la línea secundaria de preparación. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se corrigió la causa raíz por la que `Restaurar original` podía no dejar un punto de undo aunque conceptualmente fuera una operación reversible. El problema en [frontend/src/pages/useDashboardHistory.js](frontend/src/pages/useDashboardHistory.js) era que `requestHistoryCommit()` solo agenda la captura del estado siguiente; si el restore volvía a un snapshot ya existente, el historial lo veía como estado repetido y no añadía ninguna entrada nueva. Se añadió `captureHistorySnapshotNow(...)` para congelar inmediatamente el estado actual antes de restores destructivos, y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) lo usa ahora en `handleRestoreActiveSequenceOriginal()`, `handleRestartDepurationFromOriginal()` y `handleRestoreBlockOriginalOrder()`. Validación final: `get_errors` limpio en los archivos tocados.

- 2026-04-14: Se añadió un fallback automático para `hook trigger` cuando la corrida de depuración devuelve `hook` válido pero deja `hook_trigger_*` en null. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `depurate_sequence(...)` ahora intenta derivar la cola del loop desde el párrafo útil más cercano antes del hook usando `paragraph_analysis` ya devuelto por la IA y descartando spans que hayan quedado desactivados; cuando lo logra, persiste `hook_trigger_start_word_id`, `hook_trigger_end_word_id`, `hook_trigger_text` y `hook_trigger_auto_inferred=true`, y [backend/apps/editor/views.py](backend/apps/editor/views.py) lo deja explícito en el log del modal como trigger derivado automáticamente. Para cubrir también depuraciones ya guardadas, [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ahora reconstruyen ese trigger en hidratación/runtime a partir de `paragraph_analysis`, lo muestran en la revisión editorial y reparan/persisten el orden `hook -> resto -> trigger` al aplicar. Validación final: `npm run build` correcto en `frontend`, `manage.py check` correcto en `backend`, y prueba real en `project 25 / sequence seq-mnz6jc1r-thematic-16--vdah` dejando persistido `hook_trigger_start_word_id=88853`, `hook_trigger_end_word_id=88857`, `hook_trigger_text="Y un paquete de seguridad."` y `hook_loop_tail_enabled=true`.

- 2026-04-14: Se rehízo la salida del modal `Depurar secuencia` para que el tratamiento de `hook` y `trigger` deje de ser implícito y pase a ser una decisión visible del usuario. En [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx) el estado `ready` ahora muestra una revisión editorial con dos cards separadas: `Usar como hook de apertura` y `Usar como trigger al final`, ambas con checkbox activado por defecto cuando la corrida devolvió rangos válidos, además de un resumen explícito de lo que ocurrirá al aplicar. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleApplyDepurationResult(...)` dejó de asumir siempre el hook loop: ahora respeta esas opciones, persiste `hook_use_as_opening` y `hook_trigger_use_for_loop_tail` dentro del resultado guardado, y solo reordena `visualSplitPoints / visualBlockOrderIds` cuando el usuario lo confirmó. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron los estilos de esta revisión para que el bloque se lea como aprobación editorial real y no como texto diagnóstico suelto. Diagnóstico del caso real revisado en `project 25 / sequence seq-mnz6jc1r-thematic-16--vdah`: la última depuración sí devolvió `hook` (`hook_start_word_id=88861`, `hook_end_word_id=88873`), pero no devolvió `hook_trigger_start_word_id` ni `hook_trigger_end_word_id`, así que no existía trigger reutilizable para construir el loop; antes eso quedaba opaco en el modal y ahora queda visible. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se cambió la base de `fit` para que deje de abrir el source completo por defecto y arranque desde un recorte cuadrado centrado. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) `getDefaultCropPresetForMode("fit", ...)` pasa ahora a `1:1`, con lo que los nuevos clips en `fit`, el reset de crop y los planes editoriales nacen sobre un square crop centrado que luego se presenta ajustado dentro del canvas. Para evitar divergencias entre preview y render final, [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) alineó también el fallback de `fit` sin contexto fuerte a `1:1`, y [backend/apps/editor/services.py](backend/apps/editor/services.py) hizo lo mismo en los defaults de export cuando falta un visual plan explícito. Objetivo: que `fit` se lea como `encuadre editorial cuadrado centrado` y no como `source completo con barras`.

- 2026-04-14: Se corrigió la persistencia de la preferencia del estabilizador en el preview clásico y se reforzó su affordance visual. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el cambio de `Original / Suave / Estándar / Intenso` ya no vive solo en `editor session`: ahora también se guarda dentro de la secuencia activa con `captureHistory: false`, de modo que al reabrir el mismo proyecto/secuencia el preview restaura primero la preferencia persistida en draft y recién después cae al valor de sesión como fallback. Para que el draft no pierda esa preferencia durante guardado/rehidratación, [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) y [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) añadieron normalización y serialización explícita de `sequencePreviewPlaybackMode` por secuencia. En [frontend/src/styles/app.css](frontend/src/styles/app.css) los botones del strip de estabilización ganaron además un testigo puntual de color para el modo activo, inspirado en el estado activo de `Audio Fix` pero más discreto: una marca mínima en la esquina del botón, cyan para estado activo general y verde cuando el modo activo ya está `ready`. Validación final: `get_errors` limpio en los archivos tocados y `npm run build` correcto en `frontend`.

- 2026-04-14: Se reforzó la persistencia y la integridad de undo del editor clásico alrededor de cierres rápidos, cambios de secuencia y estados de proceso como `Audio Fix`. En [frontend/src/pages/useDashboardHistory.js](frontend/src/pages/useDashboardHistory.js) se añadió `resolveHistoryForSave(...)` para serializar el historial correcto incluso cuando todavía existe un commit pendiente; en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `saveProjectDraft(...)` y `persistEditorDraft(...)` pasaron a usar esa resolución antes de guardar local o hacer `PATCH` al backend, `handleCloseActiveSequenceView()` ahora persiste el draft antes de cerrar la secuencia, y el efecto que empuja persistencia al backend dejó de depender solo de `derivedBlocks` para escuchar también `sequenceDrafts` y `transcriptWordOverrides`, cerrando el hueco donde cambios no estructurales como `sequence.audioEnhancement` podían quedar solo locales. En la misma pasada, borrar secuencias y `Fundir` ahora dejan snapshot explícito de undo mediante [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js). Validación final: `get_errors` limpio, `npm run build` correcto y comprobación directa en `localStorage` del draft `video-editor:editorial-draft:25` mostrando `audioEnhancement.status=ready`, `outputUrl` persistido y `settings.useForPreview=true` para la secuencia `ai-editorial-1776109250488-35`; el smoke test visual de `Fundir -> Undo` quedó parcialmente bloqueado porque, en esta secuencia concreta, varios botones `Fundir` aparecen habilitados pero no llegan a materializar un merge real.

- 2026-04-14: Se revisó el preview clásico bajo la hipótesis de `reseeks` redundantes compitiendo con el reloj de reproducción y se cerró una causa real en [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js). El diagnóstico en navegador mostró un patrón incorrecto en transiciones: además del `seek` legítimo del slot staging para preparar el siguiente bloque, un comando viejo todavía podía volver a imponer `seek` sobre el otro slot primario y producir sensación de replay visual. La corrección añadió ownership por video (`command token` por `<video>`) y guardas de sesión para que sincronizaciones viejas de `idle` o de preview ya no puedan reseekear un slot cuando el transporte activo ya cambió de dueño. Validación final: `get_errors` limpio, `npm run build` correcto y prueba instrumentada en `http://127.0.0.1:5173/?project=25&sequence=ai-editorial-1776109250488-35` donde el handoff pasó de `2 seeks primarios en una transición` a un solo `seek` del slot staging antes del swap.

- 2026-04-14: Se compactó de nuevo el toolbar de estabilización/audio del editor clásico después de una primera versión demasiado alta para el chrome del producto. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) la estabilización volvió a una sola línea compacta (`Original` + `Suave/Estándar/Intenso` + estado inline con barra), y `Audio Fix` dejó de mostrar una pastilla muerta `Listo para render`: ahora el chip secundario es una acción real (`Render WAV`, `Usar WAV`, `Fix ON`, `Rehacer`, `Cola xx%`) en vez de texto decorativo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `Audio Fix` se alineó con el mismo patrón que estabilización: si falta master, el click deja la intención en cola, dispara la preparación de media y reanuda el render automáticamente cuando el master pasa a `ready`; además `canEnhanceSequenceAudio` dejó de bloquear la UI por no existir aún el master. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se redujo de nuevo el volumen visual del strip para mantenerlo denso y técnico. Validación final: `get_errors` limpio, `npm run build` correcto y runtime corregido tras resolver una referencia residual (`stabilizationPreviewActive is not defined`).

- 2026-04-14: Se rehízo el control de estabilización del preview clásico para que deje de ser un `chip` ambiguo y pase a comportarse como un selector operativo real. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el toolbar ahora muestra `Original`, un toggle `Estabilizado`, los tres modos `Suave/Estándar/Intenso` con estado propio (`Crear`, `En cola`, `%`, `Listo`, `Rehacer`) y una fila común de progreso con barra para master o estabilización activa; así se puede activar/desactivar el preview estabilizado, elegir el modo deseado y ver qué falta sin adivinar. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el click sobre un modo dejó de bloquearse por falta de master: si todavía no existe media preparada, el controller encola la intención del modo, dispara `master + proxy` y reanuda automáticamente la estabilización elegida cuando la secuencia pasa a `ready`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron los estilos del nuevo panel compacto, botones por modo y barra de avance compartida. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se corrigió la semántica del trim normal para que vuelva a crecer/encoger por palabras y huecos, no como una versión degradada del trim preciso. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `applyBlockTrim(...)` dejó de proyectar el borde solo contra `start_ms/end_ms` de palabras activas y pasó a evaluar también fronteras de espacio reales (`previousBlockSourceOutMs`, `nextBlockSourceInMs`) usando deltas sobre `currentSourceInMs/currentSourceOutMs`; con eso el drag normal puede absorber palabras inactivas y también silencios/gaps puros en el borde, incluyendo quitar o extender hueco sin caer al flujo exacto en ms del `Ctrl/Cmd + drag`. En la misma pasada `Fundir` dejó de habilitarse solo por selección: ahora [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) solo exponen merge cuando el clip seleccionado ya cerró el hueco hasta el vecino (`source_out_ms >= next.source_in_ms - 1` o equivalente con el anterior). Validación final: `get_errors` limpio, `npm run build` correcto y documentación actualizada para distinguir trim normal vs trim preciso.

- 2026-04-14: Se aclaró la interacción entre los dos trims del timeline y se despejó el affordance de fundido. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el `Ctrl/Cmd + drag` dejó de leer la posición absoluta del cursor sobre la regla y pasó a usar la misma distancia fantasma visible del drag (`ghostOffsetPx`), convertida a ms según el zoom actual; así el trim preciso respeta exactamente el desplazamiento visual del borde y solo difiere del trim normal en el commit final (`ms exactos` vs. absorción/liberación de palabras activas, inactivas y gaps). En [frontend/src/styles/app.css](frontend/src/styles/app.css) la etiqueta `Fundir` del puente entre clips dejó de quedar visible todo el tiempo cuando el botón aparece: ahora solo se muestra en `hover/focus` sobre el icono, despejando el acceso al trim lateral. Validación final: `get_errors` limpio, `npm run build` correcto, label del puente en reposo con opacidad `0` y visible solo al interactuar con el icono.

- 2026-04-14: Se simplificó la lectura visual del timeline sin tocar los controles útiles de restauración del transcript. En [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) se retiraron las badges `SRC/SEQ`, que estaban añadiendo ruido sin resolver una acción concreta para el editor; los clips reordenados quedan ahora explicados solo por tooltip y por la acción existente `Restaurar original`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se compactó además la franja verde/naranja del timeline (`.workflow-text-lane` y `.workflow-text-block`) para recuperar altura útil vertical sin cambiar la semántica de esos bloques. Validación final: `get_errors` limpio en los archivos tocados, recarga fresca del editor sin error boundary activo, `Restaurar original` visible y DOM del timeline confirmando `0` badges antiguas y altura efectiva de `22px` por bloque de texto.

- 2026-04-14: Se corrigió un desborde visual en el extremo derecho del timeline del editor. La causa real no era el `scroll` ni el waveform, sino que [frontend/src/styles/app.css](frontend/src/styles/app.css) imponía `min-width` rígido a `.workflow-clip-block` y `.workflow-text-block`; cuando un bloque final tenía una duración corta, su `style.width` quedaba por debajo de ese mínimo pero el render real seguía creciendo, empujando el último clip fuera del carril y haciéndolo parecer montado sobre el borde derecho. Se retiraron esos mínimos fijos para que el ancho visible respete la duración calculada del bloque. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y medición DOM confirmando que el último clip ya renderiza con el mismo ancho que su `style.width` en vez de inflarse artificialmente.

- 2026-04-14: Se aclaró la lectura de clips reordenados en el timeline del editor. La badge experimental `TL 00:43.9` resultó demasiado críptica para distinguir tiempo de origen vs. posición dentro de la secuencia, así que en [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) se reemplazó por labels explícitas: los clips reordenados ahora muestran `SRC hh:mm.s` para el tiempo original del material y `SEQ hh:mm.s` para su posición dentro del timeline editado, además de un tooltip más claro (`Posición en secuencia` vs `Origen`). Objetivo: que un loop editorial válido no se lea como corrupción visual ni como “otro tiempo misterioso” al final del timeline. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se rehízo la comunicación del toolbar `media/process` del editor clásico para que deje de mezclar `modo de preview` con `estado de pipeline`. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) se añadió un bloque persistente `Media` que siempre indica si el preview original está usando fuente base, `proxy activo`, `master activo` o si la media está `generando/error/stale/cancelada`, incluyendo progreso cuando el master+proxy están en curso y toggle directo entre proxy/HD cuando ya existen. En la misma pasada el contador ambiguo `0/3` de estabilización pasó a leerse como `Stab 0/3`, y `Audio Fix` dejó de quedar mudo en idle para mostrar estados operativos como `Listo para render`, `Espera media`, `WAV listo` o `Fix ON`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se migró además la paleta de estos controles desde ámbar/marrón hacia grises técnicos con acento cyan para alinear mejor la lectura del chrome del editor. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-14: Se corrigió la causa raíz de una apertura fallida de proyectos en la shell de biblioteca. El flujo `openProject(...)` sí alcanzaba a pedir `bootstrap` y `word-timings`, pero la hidratación del draft caía silenciosamente en `catch` antes de fijar `activeProjectId` porque [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) ejecutaba `repairHydratedSequenceHookLoop(...)` con una llamada a `deriveSequenceBlocks(...)` que no estaba importada en ese módulo. Resultado visible: la card quedaba en `Seleccionado`, pero el frame `Proyecto` seguía vacío y `Batch export` mostraba `Sin proyecto activo`. Se añadió la importación faltante y con eso la hidratación vuelve a completar el open real del proyecto.

- 2026-04-14: Se ajustó la interacción del módulo `Project List` para que abrir un proyecto no dependa únicamente del evento `dblclick` del navegador, que estaba resultando ambiguo en la shell. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la card ahora detecta explícitamente el segundo click (`event.detail >= 2`) y abre el proyecto en shell desde ese mismo flujo, mientras sigue dejando el primer click como selección. En la misma pasada se normalizó la comparación de `activeProjectId` contra `project.id` a string para que la UI no pierda el estado `abierto` por mismatch número/string, y en [frontend/src/styles/app.css](frontend/src/styles/app.css) se separó visualmente `seleccionado` (ámbar) de `abierto` (verde) con chip y badge dedicados para que quede claro cuándo un proyecto ya está cargado aunque el usuario siga dentro de la biblioteca.

- 2026-04-14: Se añadió una primera capa de `speaker focus continuity` al backend de auto frame en [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py). El motor ahora resuelve un `speaker_focus_side` por bloque cuando el transcript sugiere un solo hablante y el clip mantiene dos sujetos/lados posibles: primero intenta reutilizar memoria lateral por speaker a nivel de secuencia (`speaker label -> left/right`), y si todavía no existe mapping cae a dominancia visual, continuidad del foco previo o sujeto principal. Ese lado se inyecta antes del override de `portrait fill`, de modo que el crop pueda centrar al hablante activo con tracking suave en vez de depender solo de la cara dominante del frame. Cuando el bloque realmente se lee como diálogo a dos con evidencia fuerte, la heurística sigue dejando espacio para `split/zoom out` en lugar de forzar un cierre sobre una sola persona. Resultado esperado: mejor continuidad speaker-to-speaker entre clips y un POI más estable para portrait/fill sin perder el criterio contextual de mostrar a ambos cuando conviene.

- 2026-04-14: Se añadió además una señal real de `local activity` para distinguir quién parece estar hablando cuando hay dos sujetos visibles en el mismo plano, también en [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py). Cada sample ahora mide energía de movimiento dentro de las cajas de sujeto detectadas, no solo en mitades abstractas del frame; con eso el bloque agrega `active_speaker_side`, `active_speaker_side_bias` y `active_speaker_frame_ratio`. En la resolución de `speaker_focus_side`, esta evidencia entra antes que la dominancia visual pura cuando el transcript indica un solo speaker, de modo que el portrait/fill pueda seguir mejor al hablante real y no solo al rostro más grande o más centrado.

- 2026-04-14: Se movió la reparación del loop `hook + trigger` al nivel de hidratación del draft en [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js), no solo al runtime de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Problema real: si una secuencia ya venía guardada con `hookMovedToStart=true` pero con `visualSplitPoints/visualBlockOrderIds` legacy, al reabrirla el editor seguía mostrando `hook -> resto -> prefijo` aunque la depuración sí tuviera trigger persistido. Ahora `hydrateStoredSequences(...)` repara silenciosamente ese estado al cargar cualquier draft/historial/backend draft: reconstruye splits del trigger, recompone el orden `hook -> resto -> trigger` y limpia `movedBlockIds` a solo los bloques realmente movidos. Además se corrigió el draft local actual del proyecto 25 para que la secuencia de prueba no siga mostrando el layout viejo.

- 2026-04-14: Se alineó `Restaurar original` con el resto de restores del editor en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Aunque la ruta ya pasaba `captureHistory: true`, ahora `handleRestoreActiveSequenceOriginal()` también marca `requestHistoryCommit()` explícitamente antes de mutar la secuencia, igual que otros handlers de restore/reordenamiento. Objetivo: que apretar `Restaurar original` deje siempre un punto claro de undo y el usuario pueda volver inmediatamente al estado anterior con `Undo`.

- 2026-04-14: Se corrigió la causa raíz del loop visual `hook al frente + trigger al final` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). El estado roto venía de dos cosas a la vez: la línea disparadora no se estaba separando como bloque propio dentro de `visualSplitPoints`, así que el orden visual terminaba moviendo todo el prefijo como cola; además `movedBlockIds` seguía heredando bloques marcados de corridas previas, dejando una pieza naranja extra aunque el loop nuevo ya no la necesitara. Ahora `buildHookLoopVisualPlacement(...)` inserta splits explícitos antes y después del trigger, además de los del hook, y recalcula `movedBlockIds` solo con los bloques realmente movidos en la corrida actual. Se dejó también corregido el draft local de `project 25 / sequence ai-editorial-1776109250488-35` para que el estado persistido no siga reinyectando el layout roto mientras el editor vuelve a hidratar el proyecto correctamente.

- 2026-04-13: Se reparó el módulo de botones del preview clásico para que los submenús de `Subtitulos`, `Titulo principal` y `Audio Fix` no vuelvan a quedar recortados por el contenedor scrollable del toolbar. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) los tres menús pasaron a calcular una posición flotante fija ligada al botón ancla, de modo que siguen visibles aunque el strip horizontal tenga scroll; además el toolbar quedó reagrupado en bloques cromáticos (`view`, `media`, `text`, `meta`) para separar mejor canvas/encuadre, procesos de media, texto overlay y resumen. En [frontend/src/components/PreviewSubtitleMenu.jsx](frontend/src/components/PreviewSubtitleMenu.jsx) el menú ahora acepta `style/className` externos para esa colocación flotante, y en [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron los estilos de grupos y se limpió CSS inválido/anidado residual del bloque `Audio Fix` que podía dejar reglas posteriores en estado inconsistente. Validación final: `get_errors` limpio en los archivos tocados y `npm run build` correcto en `frontend`; la verificación visual completa del editor quedó parcialmente limitada porque, tras la recarga, el navegador integrado perdió la sesión activa del proyecto y empezó a abortar rutas de preview del backend.

- 2026-04-13: Se cerró un borde de persistencia cross-browser para secuencias aprobadas en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). El problema observado era consistente con un draft más nuevo viviendo solo en `localStorage` de un navegador mientras otro navegador abría el mismo proyecto desde `project.editor_draft` en base y no veía la última secuencia aprobada. Ahora `persistEditorDraft(...)` devuelve si el `PATCH /api/editor/projects/:id/` realmente fue aceptado por backend y `openProject(...)` re-sincroniza automáticamente al servidor cualquier draft local más nuevo que el persistido en base, de modo que una secuencia aprobada que haya quedado local por un fallo transitorio de red/servidor vuelva a subir al `editor_draft` y quede visible en otros navegadores. Verificación puntual de datos en este caso: el proyecto `25` tenía `11` secuencias persistidas en base, así que la supuesta `secuencia 12` no estaba todavía en DB al momento del diagnóstico.

- 2026-04-13: Se cubrió un segundo origen local del mismo problema cross-browser en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). El primer fix solo resincronizaba el draft estándar; si un navegador abría desde `restore snapshot` local más nuevo que la base, seguía viendo la secuencia faltante sin volver a subirla al backend. Ahora `openProject(...)` compara también `restoreSnapshot.payload.savedAt` contra `bootstrap.editor_draft.savedAt` y reenvía esa restauración al backend cuando es más nueva, cerrando el caso en que Edge conserva la secuencia aprobada dentro de una restauración local pero la DB sigue en 11.

- 2026-04-13: Se aplicó un recorte real del arranque de `slide 1` para transmitir más velocidad y agilidad en la shell inicial. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `loadProjects()` dejó de pedir `/api/editor/bootstrap/` al entrar; ahora la raíz carga solo `/api/editor/projects/` y el bootstrap pesado queda reservado para `openProject(...)` o restauraciones explícitas. En la misma pasada se eliminó otro eager-load silencioso del shell: el panel de `Batch export` ya no toma `projects[0]` por defecto para disparar `exports/` y `bootstrap/` de un proyecto que el usuario no abrió. En backend, [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py) ganó `ProjectListSerializer`, que devuelve un resumen de proyecto con `sequence_count` y `videos` resumidos, sin arrays completos de secuencias; además la miniatura de lista dejó de forzar `ensure_video_preview_thumbnail(...)` durante serialización y ahora solo usa la URL si el thumbnail ya existe. [backend/apps/editor/views.py](backend/apps/editor/views.py) pasó a servir ese serializer liviano en `GET /api/editor/projects/` con `prefetch_related("videos", "sequences")`. Medición previa: `/api/editor/projects/` pesaba ~`19 KB`, mientras `/api/editor/bootstrap/` estaba en ~`2.41 MB`; validación final en navegador tras recarga fresca de `http://127.0.0.1:5173/`: los únicos recursos API observados al entrar fueron requests a `/api/editor/projects/`, sin `bootstrap` global ni `projects/<id>/bootstrap` implícito. Checks finales: `npm run build` correcto en `frontend`, `manage.py check` correcto en `backend` y `get_errors` limpio en los archivos tocados.

- 2026-04-13: Se verificó en navegador otra falsa rotura de CSS del editor clásico que no venía de colisión entre clases de módulos ni de cambios nuevos en `app.css`, sino de una hoja HMR stale inyectada por Vite. El síntoma visible era severo: [frontend/src/styles/app.css](frontend/src/styles/app.css) ya contenía `.workflow-editor-grid { display: grid; ... }`, pero en el browser esa regla no aparecía en el CSSOM activo, así que transcript/preview/timeline caían a flujo normal y el timeline se veía como texto crudo. Se reinició limpio el dev server de `frontend` y, tras volver a abrir `?project=25&sequence=editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1`, el CSSOM volvió a incluir `.workflow-editor-grid`, el layout recompuso transcript a la izquierda, preview a la derecha y timeline abajo, y la captura final confirmó restauración visual. No hubo que tocar ids/clases de módulos: la independencia entre `.workflow-sequence-transcript-panel`, `.workflow-preview-column`, `.workflow-timeline-shell` y `.workflow-editor-grid` se mantuvo intacta. Validación final: `get_errors` limpio, Vite reiniciado en `127.0.0.1:5173` y comprobación visual real en navegador.

- 2026-04-13: Se reordenó el `layout vertical` del editor clásico para que deje de comportarse como `módulos arriba + timeline ancho completo abajo`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el preview y el timeline se factorizaron como nodos reutilizables para reubicarlos según el preset del workbench: en vertical, el timeline ahora se monta dentro de la grilla del editor junto al transcript, mientras el preview queda ocupando toda la columna derecha; en horizontal se conserva el flujo anterior. En [frontend/src/styles/app.css](frontend/src/styles/app.css) la variante `.workflow-editor-stack.is-vertical-layout` pasó a ser una sola superficie y la grilla interna define tres filas (`transcript`, divisor horizontal, `timeline`) por dos columnas útiles (`transcript/timeline` a la izquierda, `preview` a la derecha con span completo). Los dos resizers siguen activos: el vertical entre transcript y preview, y el horizontal solo entre transcript y timeline dentro de la columna izquierda. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`; la comprobación visual automatizada no fue concluyente porque la pestaña actual quedó montada sobre la shell general y no sobre una sesión activa del editor.

- 2026-04-13: Se redefinió el objetivo de Audio Fix para dejar de tocar ecualización, supresión de ruido y sibilancia, y pasar a un flujo de pura dinámica/salida. En [backend/apps/editor/views.py](backend/apps/editor/views.py) la cadena real ya no aplica `highpass`, `lowpass`, `afftdn/anlmdn`, `deesser` ni `dynaudnorm/speechnorm`; ahora se limita a compresión (`acompressor`), ganancia (`volume`), loudness objetivo (`loudnorm`) y limitador (`alimiter`). En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) los defaults y presets quedaron alineados con esa lógica, y en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el panel expone solo controles de `Ganancia`, `Compresión`, `Volumen objetivo` y `Limiter final`. La intención acordada: que los pasajes bajos suban a un nivel normal, los pasajes altos bajen a un rango normal sin saturar, y la voz gane un poco de presencia sin suprimir ambiente ni recortar agudos. Validación: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se redujo el bloqueo al refrescar una URL directa del editor clásico con `?project=...&sequence=...`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), `openProject()` ya no espera a que termine `loadOpenAITrace(...)` para resolver la apertura; el takeover al editor queda libre apenas el bootstrap, las palabras y la secuencia activa quedan hidratados, mientras thumbnails/waveforms y trazas complementarias siguen cargando en segundo plano. Esto ataca la sensación de “primero llena slides/thumbnails y recién después abre el editor” cuando en realidad ya venía una secuencia activa en la ruta. Validación: `get_errors` limpio y `npm run build` correcto en `frontend`; la comprobación visual en navegador quedó parcialmente limitada por la sesión dev actual (`localhost:5173` respondió pero el DOM no terminó de montar la app sobre la pestaña automatizada).

- 2026-04-13: Audio Fix ahora soporta reproceso destructivo y un catálogo más amplio de presets desde el preview clásico. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el botón detecta modificadores: `Ctrl+click` borra el WAV actual y relanza el render con la configuración vigente, mientras `Shift+click` abre un submenú de presets listos para procesar; el mismo `Ctrl` también fuerza `borrar + rehacer` sobre cada preset individual y el botón/menú cambian visualmente a un estado rojizo o cyan mientras las teclas están activas. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se ampliaron los presets editoriales a escenarios más concretos (`Natural`, `Balanceado`, `Voz al frente`, `Podcast cálido`, `Ambiente cuidado`, `Sala reverberante`, `Locación ruidosa`, `Entrevista calle`, `Audio de celular`, `Voz sibilante`, `Broadcast ajustado`, `Limpieza suave`), además del flujo `resetFirst` y el wiring para persistir el preset activo. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `sequence-audio-enhancement-control` ya acepta `action=reset`, limpia el output anterior y permite relanzar un render nuevo sobre la misma secuencia. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se conectó por fin la capa que evita que el clip entrante pinte su primer frame con el crop del clip saliente. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ya existía `applyClipStylesImperatively(...)`, pensada para calcular `displayMode`, `cropRect`, frame style y video style antes del seek, pero estaba huérfana y no se invocaba desde ningún sitio; con el sistema A/B actual eso dejaba que el slot entrante heredara visualmente el estilo previo hasta que React recomputara `previewPrimaryResolved*Style`. Ahora [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) expone refs separadas para los frames `slot A/B`, [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) aplica estilos por slot y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) los fuerza tanto al arrancar playback como justo antes del seek del slot staging en cada handoff. Resultado esperado: el video llega con el ajuste correcto en el primer frame visible, en vez de verse primero el clip y después el recorte. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se cerró otro camino duplicado de reseek en el preview clásico. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) seguía vivo un `useEffect` de `mounted auxiliary sync` que podía reasignar `currentTime` sobre backdrop/split-secondary incluso con el playback activo, en paralelo al transport explícito de `jump/start/handoff`; eso dejaba demasiado abierta la autoridad sobre el tiempo del preview y reintroducía la sensación de resincronizaciones inesperadas. Ahora esa sincronización montada solo corre en `idle`, con threshold fijo de `12ms`, y durante reproducción activa el preview visual queda gobernado únicamente por `seekSequencePreviewVideo(...)` desde las rutas explícitas del transport. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y re-instrumentación del browser sin nuevos seeks de video durante la ventana corta de reproducción normal; el único camino de corrección que sigue activo en playback es el audio mejorado vía `syncSequenceEnhancedAudioTransport(...)`.

- 2026-04-17: Se desactivó la auto-regeneración agresiva del `master/proxy` al abrir una secuencia o al detectar media `stale`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleOpenSequenceInClassicEditor(...)` ya no llama `handleTriggerSequenceClipPreparation(...)` directamente; ahora solo sincroniza estado existente. Además, el efecto `ensureSequenceClipPreparation()` dejó de relanzar `handleTriggerSequenceClipPreparation(...)` cuando la media preparada tiene `source_signature` vencida o cuando el último run quedó en `error/cancelled`; en esos casos ahora solo deja un mensaje informativo para que la regeneración ocurra únicamente por una acción explícita del usuario o por una operación que realmente la necesita (por ejemplo estabilización o Audio Fix). Motivo: evitar consumo de recursos y re-encodes involuntarios cada vez que el draft cambia levemente.

- 2026-04-17: Se recalibraron los presets de estabilización de secuencia para priorizar look editorial suave sobre zoom agresivo. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `_STABILIZATION_MODES` pasó a usar más `smoothing` progresivo y menos crop fijo: `suave` quedó en `smoothing=20, zoom=0, optzoom=1`, `estandar` en `smoothing=36, zoom=2, optzoom=1`, e `intenso` en `smoothing=52, zoom=4, optzoom=1`. También se endurecieron levemente `shakiness`/`accuracy` para detectar mejor vibración de cámara en mano. Motivo: el preset `intenso` anterior aplicaba demasiado zoom (`12`) pero menos suavizado temporal que `estandar`, lo que hacía visible rebote residual pese al crop. Riesgo: cualquier estabilización ya generada mantiene parámetros viejos; hay que relanzar el modo para ver el cambio.

- 2026-04-17: Se ajustó otra causa raíz del efecto de replay al cruzar cortes en el preview clásico. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) `resolveAutoAdvanceSourceMs(...)` ya no intenta conservar continuidad del cambio de clip usando solo el tiempo fuente del bloque saliente cuando el preview corre sobre media de secuencia concatenada (`sequence-original` / `sequence-stabilized`); ahora deriva la continuación desde el `currentTime` real del video sobre timeline de secuencia y la proyecta al bloque siguiente dentro de una ventana más amplia (`420ms`). Con esto el handoff deja de caer innecesariamente a `source_in_ms` del siguiente clip en cambios adyacentes donde el decoder ya venía algunos frames adentro, reduciendo la sensación de `arranca y vuelve a arrancar`. Validación: `get_errors` limpio en el transport; falta prueba subjetiva prolongada de playback continuo para medir si quedan cortes problemáticos fuera de esta ventana.

- 2026-04-13: El preview clásico ahora puede reiniciar correctamente desde el inicio cuando ya quedó parado al final de la secuencia y además suma un modo `repeat`. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) `playSequenceFromCurrentOrStart(...)` detecta el estado `EOF` y vuelve a arrancar desde el primer bloque en vez de intentar resumir desde el borde final; en el handoff de fin de secuencia, si `repeat` está activo, el transport hace loop al bloque `0` sin caer a `stopPlayback(...)`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadió el estado `sequencePreviewRepeatEnabled` y se cableó tanto al transport como al playerbar, y en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) apareció el toggle visual de repeat con estado activo. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el botón activo quedó marcado en verde para lectura rápida. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se corrigió otra fuente real del efecto `inicia -> reinicia` al pasar de un clip a otro en reproducción continua del editor clásico. En [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), `previewModeTransitionPending` seguía disparando una resincronización de auxiliares pensada para transiciones idle aun cuando el playback ya estaba activo; eso duplicaba trabajo sobre `split secondary` / backdrop al mismo tiempo que el handoff manual entre slots ya estaba ocurriendo dentro del transport. Además, en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `syncSequenceEnhancedAudioTransport(...)` estaba corrigiendo el audio mejorado con un umbral de deriva de solo `0.12s`, lo que provocaba micro-seeks audibles durante playback normal y hacía más notorio el “reinicio” percibido al cruzar cortes. Ahora la resincronización auxiliar por efecto queda limitada a modo idle, el handoff activo limpia explícitamente `previewModeTransitionPending` al terminar, y el audio mejorado solo corrige deriva más grande durante playback activo (`0.32s`). Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y prueba instrumentada en navegador sobre el clip split 10 de la secuencia activa mostrando que desaparecieron los micro-seeks repetidos del audio y que el secondary split ya no hace reseeks extra al arrancar.

- 2026-04-13: Se corrigió una regresión específica del modo `split` en el preview clásico. El análisis del caso real `project=25 / sequence=editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1`, clip `10` (`V split`), confirmó que el editor no estaba usando `master` por error: los tres videos del preview (`primary active`, `primary staging` y `split secondary`) apuntaban a `sequence_media/.../proxy.mp4?_r=1`. La causa del lag venía por otro lado: [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) estaba pausando los auxiliares recién sincronizados incluso cuando el playback seguía activo, así que el panel secundario de `split` podía quedar frenado o resincronizado tarde; además [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) conservaba un `useEffect` residual que seguía resincronizando `splitSecondaryVideoRef` por fuera del transport nuevo. Se cambió el transport para mantener reproduciendo los auxiliares durante playback activo y se eliminó ese efecto duplicado del page controller. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y comprobación en navegador de que, al arrancar el clip split 10, tanto el panel principal como el secundario quedan avanzando sobre `proxy.mp4`.

- 2026-04-13: Se cerró la causa raíz del blur ausente en `fit` dentro del editor clásico en [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js). El backdrop no estaba oculto: los `<video.workflow-preview-backdrop-video>` se remountaban al volver a `fit`, pero `assignSequencePreviewVideoSource(...)` solo comparaba la firma persistida en `sourceRef` y no el `src` real del nodo DOM, así que un elemento nuevo podía quedar montado con `src=""` aunque la fuente lógica no hubiese cambiado. Ahora el helper también resincroniza cuando el `src` del elemento no coincide con la URL esperada. Validación: `get_errors` limpio y `vite build` correcto; la verificación visual quedó limitada porque la pestaña `5174` estaba con `ERR_CONNECTION_REFUSED` y en `5173` no montó la superficie del preview durante esta pasada.

- 2026-04-13: Se corrigió una regresión visible del playback del editor clásico en [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js). El transporte estaba ocultando el backdrop blur justo al arrancar clips `fit` (`setPreviewBackdropHidden(startBlockDisplayMode === "fit")`), lo que dejaba al modo `fit` sin fondo blur durante reproducción; además el auto-advance entre clips siempre forzaba el siguiente bloque a `source_in_ms`, aunque el decoder ya hubiese caído unos frames adentro del mismo stream/parte, produciendo el efecto de “arranca y vuelve a arrancar” al cruzar cortes. Ahora el backdrop se mantiene visible para `fit` al iniciar playback y el handoff entre clips conserva una pequeña ventana de continuidad cuando el siguiente bloque comparte stream con el actual. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se cerró otro efecto puente dentro de `Classic preview transport`. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ya no mantiene el `useEffect` que liberaba recursos del preview cuando `activeSequenceId` quedaba vacío; esa responsabilidad pasó a [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), junto al resto del lifecycle del player clásico. En la misma pasada se limpió el destructuring residual de helpers del transport que ya no tenían consumidores reales en la página. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se consolidó otro borde de `Classic preview transport`. La pausa y liberación de recursos del preview (`pausePreviewVideos(...)` y `releaseSequencePreviewVideoResources(...)`) salieron de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y pasaron a [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), que ya es dueño del doble buffer principal, backdrop y secondary preview. Con esto el controller pierde otro bloque de housekeeping interno del player. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y recarga fresca del editor montando sin caída activa; el panel histórico de runtime todavía conserva el error previo de HMR (`pausePreviewVideos is not defined`) aunque ya no se reproduce tras la recarga.

- 2026-04-13: Se completó otro recorte de ownership dentro de `Classic preview transport`. La sincronización reactiva del audio del preview clásico (`syncSequencePreviewAudio`) salió de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y ahora vive dentro de [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), que ya era dueño de los videos `active/staging`, del secondary/backdrop y del handoff de reproducción. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) deja así otro bloque puente menos y conserva solo el transporte del audio mejorado donde todavía corresponde. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se corrigió otro borde del flujo `stream estabilizado + click del botón` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Al iniciar una estabilización por primera vez, el frontend dejaba `stabilizationJobs[mode] = null` hasta que respondiera el backend, así que el botón podía sentirse muerto o sin feedback inmediato aunque el click sí hubiera disparado la request. Ahora el modo elegido entra en estado `processing` de forma optimista desde el primer click (`Preparando estabilización...`), tanto en primera corrida como en rerun, haciendo visible la intención del usuario sin esperar la primera respuesta del backend. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y runtime sin errores reportados.

- Classic editor: añadí un divisor arrastrable entre transcript y preview, con persistencia del ancho por layout, y rehice el scrollbar horizontal del toolbar para que se vea más plano y más parecido a un fader físico.
- 2026-04-13: Se consolidó otro borde de `Preview and crop interaction`. El sync del video del modal `Crop` salió de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y pasó a [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx), que ya era dueño del estado `previewCropModalOpen` y del ciclo de interacción del modal. En la misma pasada se limpió el wiring residual del controller (`cropModalSequencePreviewVideoUrl` redundante y helpers del transporte clásico ya no usados). Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y runtime sin errores reportados.

- 2026-04-13: Se completó otro subcorte importante de `Classic preview transport`. Los efectos de sync `mounted/idle` del preview clásico salieron de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y ahora viven dentro de [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), usando directamente `previewLayoutBlock`, `previewPlaybackBlock`, `previewSourceMs` y el estado de transición/playback que ya pertenecen a ese dominio. Con esto `DashboardPage.jsx` pierde dos efectos puente más y queda más cerca de ser solo orquestación. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y runtime sin errores reportados tras recarga fresca.

- 2026-04-13: Se completó otro subcorte del wiring entre `Multipart video transport` y `Classic preview transport`. La sincronización del `sequencePreviewPartIndex` hacia las refs activas del doble buffer (`primary` y `backdrop`) salió de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y quedó dentro de [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js), que es el módulo que realmente posee esas refs activas. Con esto `DashboardPage.jsx` pierde otro efecto puente de sincronización interna del preview clásico. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`; la recarga fresca en `5173` siguió montando el shell sin caída activa del render.

- 2026-04-13: Se completó un subcorte adicional de `Multipart video transport`. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de mantener un estado duplicado para `sourcePreviewDurationMs` y de copiar manualmente el valor derivado desde `useMultipartPreviewTransport.js`; ahora consume directamente `multipartSourcePreviewDurationMs` como duración resuelta del preview fuente. Es un recorte pequeño pero útil porque elimina glue redundante del page controller y deja más claro que la duración multipart ya pertenece al hook. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se corrigió un borde real entre `Preview and crop interaction` y los modos de estabilización del preview clásico. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) estaba calculando `cropModalSequencePreviewVideoUrl` con preferencia por la URL estabilizada activa (`suave` / `estandar` / `intenso`), lo que hacía que el modal `Crop` quedara negro o no usable fuera de `Original`. El modal volvió a fijarse siempre sobre `sequencePreviewVideoUrl` (fuente base o media preparada de secuencia) y su seek inicial se simplificó para distinguir solo entre timeline preparado vs. fuente multipart/original. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y runtime sin errores reportados tras recarga fresca de `5173`.

- 2026-04-13: Se corrigió la regresión runtime que quedó abierta tras extraer `Classic preview transport`: [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) estaba usando `normalizeClipStabilizationSettings(...)` sin mover también ese helper fuera de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La normalización se extrajo a [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) y ambos consumidores pasaron a importar desde esa fuente compartida, eliminando el `ReferenceError` del preview (`Cannot access/normalizeClipStabilizationSettings is not defined`) sin volver a duplicar lógica. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend`, recarga real de `http://127.0.0.1:5173/` sin runtime errors reportados.

- 2026-04-13: Se abrió el primer corte real de `Multipart video transport`. [frontend/src/pages/useMultipartPreviewTransport.js](frontend/src/pages/useMultipartPreviewTransport.js) absorbió la resolución base multipart (`multipartSourceParts`, `resolveMultipartPartIndex(...)`, `getMultipartPartByIndex(...)`, `getSourcePartGlobalMs(...)`, `getSourcePartLocalSeconds(...)`), los helpers `seekMultipartVideo(...)` / `playMultipartVideo(...)`, el sync automático de `sequencePreviewPartIndex` y `sourcePreviewPartIndex`, y el transporte completo del `range detail preview` (`handleRangeDetailPreviewToggle(...)`, `handleRangeDetailPreviewScrub(...)`, volumen/mute y playback state). [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de ser dueño directo de ese bloque multipart y bajó a `16925` líneas; el nuevo hook quedó en `333` líneas. En la misma pasada [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) empezó a bloquear interacciones redundantes del toolbar durante transiciones del preview (`previewBackdropTransitionPending` / `previewModeTransitionPending`) para evitar clicks repetidos mientras el player todavía está resincronizando. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-13: Se abrió el primer corte real de `Preview and crop interaction`. [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) absorbió el estado y handlers principales del preview editable: `previewSnapGuides`, `previewSelectionVisible`, `previewCropModalOpen`, `previewCropTrackingDrawMode`, la coordinación global de drag/pointer del preview/crop/tracking, `handlePreviewSurfacePointerDown(...)`, `handlePreviewScalePointerDown(...)`, `handleOpenCropModal(...)`, `handleCloseCropModal(...)`, `handleApplyCropModal(...)`, `handleCropRectPointerDown(...)`, `handleTrackingRectPointerDown(...)`, `handleTrackingResizeHandlePointerDown(...)`, `handleCropResizePointerDown(...)`, `handleSelectedClipDisplayModeChange(...)` y `renderPreviewSnapGuides(...)`. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó re-cableado para consumir ese dominio por hook en vez de implementarlo inline. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `17059` líneas, [frontend/src/pages/usePreviewInteractionWorkspace.jsx](frontend/src/pages/usePreviewInteractionWorkspace.jsx) en `761` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `990` líneas. Nuevo estado: `Preview and crop interaction` pasa a `active`; el siguiente frente principal del plan queda entre consolidar ese hook con los bordes restantes del preview y abrir `useMultipartPreviewTransport.js`.

- 2026-04-13: Se completó un quinto subcorte dentro de `Classic preview transport`. [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) absorbió la capa de media source/seek del preview clásico: `buildSequencePreviewMediaSource(...)`, `getSequencePreviewReferenceMs(...)`, `getSequencePreviewComparableMs(...)`, `getSequencePreviewSourceTargetSeconds(...)`, `getSequencePreviewVideoGlobalMs(...)`, `syncSequencePreviewPartIndexRef(...)`, `assignSequencePreviewVideoSource(...)`, `seekSequencePreviewVideo(...)` y `playSequencePreviewVideo(...)`. Con eso [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de implementar inline prácticamente todo el transporte clásico y pasó a consumir esos helpers por retorno del hook. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`. La telemetría del navegador quedó contaminada por reinicios/fallos previos del dev server (`vite`/HMR con `ERR_CONNECTION_REFUSED` y `ERR_ABORTED`), así que esta pasada queda validada principalmente por build limpio. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `17693` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `990` líneas. Nuevo estado: `Classic preview transport` queda prácticamente consolidado; lo que resta del plan principal se concentra mucho más en `usePreviewInteractionWorkspace.js` y `useMultipartPreviewTransport.js`, con algunos remates menores todavía dispersos en `DashboardPage.jsx`.

- 2026-04-13: Se completó un cuarto subcorte dentro de `Classic preview transport`. [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) absorbió la lógica operativa de sincronización montada del preview secundario y del sync idle del primario; [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) conserva todavía los `useEffect`, pero ya no implementa inline el trabajo de sincronizar backdrop/split-secondary o primario/staging. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `17833` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `735` líneas. Nuevo estado: dentro de `Classic preview transport` queda sobre todo la capa de media source/seek (`buildSequencePreviewMediaSource`, `seekSequencePreviewVideo`, `playSequencePreviewVideo`) y el wiring final con interacción/crop/multipart; una vez movida esa parte, el siguiente frente principal pasa a `usePreviewInteractionWorkspace.js`.

- 2026-04-13: Se completó un tercer subcorte dentro de `Classic preview transport`. [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) absorbió `jumpVideo(...)`, que ya dependía casi por completo del `slot router` extraído en la pasada anterior. Con eso [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de ser dueño del salto imperativo que resincroniza primario, staging, backdrop y split-secondary al scrubbing/cueing. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`. La recarga del editor siguió acumulando ruido previo de HMR en consola (`Failed to reload` / warning de orden de hooks), así que este corte queda validado principalmente por build limpio y por no introducir nuevos errores de editor; el siguiente paso recomendado es mover los efectos de sync idle/transition que siguen consumiendo `seekSequencePreviewVideo(...)` desde el page controller y luego repetir una verificación runtime fresca. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `17877` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `632` líneas.

- 2026-04-13: Se completó un segundo corte dentro de `Classic preview transport`. [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) absorbió además el `slot router` del preview clásico: accessors `active/staging` para primario/backdrop, setters de `partIndex`, `resolveSequencePreviewBlockForSourceMs(...)` y `setPreviewBackdropHidden(...)`. Con esto [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de ser dueño directo de esa capa de routing interna del transporte y pasó a consumirla por destructuring desde el hook, mientras la lógica de media source/seek sigue todavía en el page controller. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y recarga real del editor en `http://127.0.0.1:5173/?project=25` montando sin crash; durante la edición quedó un warning de orden de hooks en el log de HMR, pero no reapareció como fallo bloqueante en la recarga fresca. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `17944` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `533` líneas. Siguiente subcorte recomendado: mover desde `DashboardPage.jsx` la capa `jumpVideo(...)` + sync idle/transition de previews o, si conviene cambiar de frente, abrir `usePreviewInteractionWorkspace.js`.

- 2026-04-13: Se abrió el primer corte real de `Classic preview transport`. [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) ahora concentra `stopPlayback(...)`, `playSequenceFromCurrentOrStart(...)` y el `syncSecondaryPlayback(...)` interno del preview clásico, incluyendo el handoff con doble buffer `slot A/B`, el preroll del blur staging y la sincronización del panel secundario. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó solo el wiring mediante wrappers con `ref`, para no reabrir dependencias del `Sequence editor workspace` ni tocar todavía crop/scrub/interacción del preview. Durante la integración apareció una regresión real de orden (`Cannot access 'playbackRef' before initialization`) al montar el hook demasiado arriba; se corrigió moviendo la llamada a `useClassicPreviewTransport(...)` por debajo de `playbackRef` y de los refs de preview multipart. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y smoke test de runtime en `http://127.0.0.1:5173/?project=25` sin `pageErrors` tras la corrección. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `18016` líneas y [frontend/src/pages/useClassicPreviewTransport.js](frontend/src/pages/useClassicPreviewTransport.js) en `421` líneas. Nuevo estado: `Classic preview transport` pasa a `active`; la siguiente frontera lógica es separar `usePreviewInteractionWorkspace.js` o terminar de vaciar refs/helpers residuales del transporte clásico.

- 2026-04-12: Se cerró prácticamente toda la coordinación interna del timeline dentro de `Sequence editor workspace`. [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) absorbió `selectTimelineBlockByIndex(...)`, `moveSelectedTimelineBlockByOffset(...)`, `adjustSelectedTimelineBlockBoundaryByWord(...)`, `handleTranscriptBoundaryArrowShortcut(...)` y `handleTimelineArrowShortcut(...)`, con lo que el workspace del editor ya concentra trim activo fino, reorder por teclado y la navegación principal del timeline. Para evitar nuevos problemas de orden, el hook pasó a consumir getters diferidos para refs/colecciones activas (`focusedSequenceWordIdRef`, `sequenceKeyboardSurfaceRef`, `sequenceTranscriptItemById`, `derivedBlocksRef`) y callbacks imperativos (`jumpVideo`, `setSequencePlayheadMs`). Validación final: `get_errors` limpio, `npm run build` correcto y smoke test real en `http://127.0.0.1:5173/?project=25` sin `pageErrors` ni `consoleErrors`. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `19514` líneas y [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) en `958` líneas. Nuevo estado: el dominio `Sequence editor workspace` queda casi cerrado; el peso restante del refactor se desplaza a `useClassicPreviewTransport.js`, `usePreviewInteractionWorkspace.js` y `useMultipartPreviewTransport.js`.

- 2026-04-12: Se cerró otra capa del dominio `Sequence editor workspace`. [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) ahora también es dueño del cluster de split del editor clásico: `handleSplitAtPlayhead(...)`, `handlePreciseSplitAtPlayhead(...)`, `handleVisualOnlySplitAtPlayhead(...)` y `handleSplitAtPlayheadWithMode(...)` salieron de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). El hook recibió además helpers/constantes puros del dominio (`buildSourcePosition`, `cloneClipDisplayStateToBlock`, `buildMutableClipDisplayState`, `finalizeClipDisplayState`, threshold de gaps y setters de status/selección) sin romper el orden de inicialización. Validación final: `get_errors` limpio en ambos archivos, `npm run build` correcto en `frontend` y smoke test real en navegador sobre `http://127.0.0.1:5173/?project=25` sin `pageErrors` ni `consoleErrors`. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `19762` líneas y [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) en `668` líneas. Siguiente subcorte activo: trim activo fino (`adjustSelectedTimelineBlockBoundaryByWord(...)`) y coordinación de teclado/timeline.

- 2026-04-12: Se completó el siguiente subcorte del dominio `Sequence editor workspace`. [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) absorbió además `handleDeleteSelectedTimelineBlock(...)`, `handleMergeBlocks(...)` y `handleTrimHandlePointerDown(...)`, dejando a [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) sin la capa de borrado/merge/arranque de trim del timeline. Para mantener estable la inicialización del page controller, el hook pasó a consumir getters diferidos adicionales para dependencias tardías (`pushUndoSnapshot`, selección activa, `activeSequence`, decorators/editorial context) en lugar de capturar valores aún no inicializados; esto corrigió dos regresiones reales de runtime (`Cannot access 'pushUndoSnapshot' before initialization` y los TDZ anteriores del workspace). Validación final: `get_errors` limpio en ambos archivos, `npm run build` correcto en `frontend` y smoke test real en navegador sobre `http://127.0.0.1:5173/?project=25` sin `pageErrors` ni `consoleErrors`. Medición tras este corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `19993` líneas y [frontend/src/pages/useSequenceEditorWorkspace.js](frontend/src/pages/useSequenceEditorWorkspace.js) en `416` líneas. Siguiente subcorte activo: `handleSplitAtPlayheadWithMode(...)`, navegación/keyboard del timeline y el resto del trim activo (`adjustSelectedTimelineBlockBoundaryByWord(...)`).

- 2026-04-12: Se cerró el dominio de intake/proyecto dentro del plan de modularización. En [frontend/src/pages/useProjectIntakeWorkflow.js](frontend/src/pages/useProjectIntakeWorkflow.js) el módulo ya no solo concentra los handlers operativos (`create`, `append`, `extract-part` y preview local del archivo), sino también el estado transitorio del intake mediante `useProjectIntakeState(...)`: `uploadForm`, `uploadVideoPreviewUrl`, `isUploadingProject`, `isAppendingProjectVideo`, `appendProjectVideoTarget` y `extractingProjectPartId` salieron de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). En la misma pasada se restauró el bloque de imports de componentes del shell que se había mutilado mientras se estabilizaba el runtime, dejando de nuevo el dashboard montando en `http://127.0.0.1:5173/`. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y snapshot real del navegador mostrando el shell completo cargado. Medición actual: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `20226` líneas y [frontend/src/pages/useProjectIntakeWorkflow.js](frontend/src/pages/useProjectIntakeWorkflow.js) en `296` líneas. Siguiente corte activo: `useSequenceEditorWorkspace.js`.

- 2026-04-12: Se abrió el siguiente corte del plan de modularización del dashboard con [frontend/src/pages/useProjectIntakeWorkflow.js](frontend/src/pages/useProjectIntakeWorkflow.js). En esta primera pasada el hook nuevo absorbió el comportamiento operativo del intake de proyecto que seguía inline en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): preview local del archivo seleccionado, creación de proyecto, append de video a proyecto existente y separación de una parte a proyecto nuevo. `DashboardPage.jsx` mantiene todavía el estado transitorio (`uploadForm`, locks y target selection), así que el área queda marcada como `active` y no `done`. Validación final: `get_errors` limpio en ambos archivos y `npm run build` correcto ejecutado desde `frontend`; medición actual: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `20177` líneas y [frontend/src/pages/useProjectIntakeWorkflow.js](frontend/src/pages/useProjectIntakeWorkflow.js) en `265` líneas. Siguiente paso recomendado: mover el estado transitorio restante del intake o saltar ya al corte grande de `useSequenceEditorWorkspace.js`.

- 2026-04-12: Se ejecutó el siguiente corte real del plan de modularización del dashboard. En [frontend/src/pages/useProjectTranscriptionWorkflow.js](frontend/src/pages/useProjectTranscriptionWorkflow.js) quedó extraído el subdominio de transcripción de proyecto: polling del worker, control cooperativo (`pause/cancel/resume`), retranscripción, borrado/restauración de transcript y los efectos de autoapertura/cronómetro del modal de estado. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) conserva ahora el estado derivado del modal y el wiring visual, pero ya no es dueño directo de esos handlers. Medición tras el corte: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó a `18952` líneas y el nuevo hook quedó en `633` líneas. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`. Siguiente corte activo según el mapa: `useProjectIntakeWorkflow.js`.

- 2026-04-12: Se cerró otra capa del problema de links compartidos `?project=<id>&sequence=<id>` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Primero, `openProject(...)` ahora sintetiza un draft mínimo cuando la secuencia existe de forma persistida pero `bootstrap.editor_draft.sequences` llega vacío, lo que permite mantener `activeSequenceId` y conservar `sequence=` en la URL al abrir secuencias vacías desde ruta directa. Segundo, se añadió una guarda contra aperturas obsoletas de proyecto (`openProjectRequestIdRef`) para que resultados tardíos no vuelvan a sobrescribir el estado visible del shell con un bootstrap viejo. En la misma pasada se dejó una recarga correctiva al salir del takeover si el `bootstrap.project` visible no coincide con `activeProjectId`. Validación final: carga limpia en `/?project=12&sequence=17` entrando al editor clásico, salida a selector manteniendo el proyecto correcto tras red inactiva, sin runtime errors reportados y `npm run build` correcto en `frontend`. Hallazgo adicional de tracing: los `net::ERR_ABORTED` observados durante navegación no venían del historial de exports, sino de thumbnails de biblioteca (`thumbnail_url` / `video-preview`) que el navegador cancela al desmontar o reemplazar vistas.

- 2026-04-11: Se corrigió una regresión del transporte del editor clásico en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) que dejaba el video avanzando pero congelaba la línea del timeline y la palabra activa del transcript cuando el preview usaba media preparada de secuencia (`sequence-original` / `sequence-stabilized`). La causa raíz era una mezcla de sistemas de coordenadas: `getSequencePreviewVideoGlobalMs(...)` devolvía `currentTime` como si ya fuera `sourceMs`, pero en ese modo el reloj del `<video>` está en tiempo de secuencia; el loop de playback restaba luego `source_in_ms`, con lo que `nextSequenceMs` quedaba pegado al inicio del bloque. Ahora el helper vuelve a mapear ese reloj a `sourceMs` real para el loop, mientras una ruta separada conserva la comparación en tiempo de secuencia para el drift de videos auxiliares. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`; la verificación interactiva en navegador quedó limitada por inestabilidad del host Vite (`127.0.0.1` caído y recarga en `localhost` en blanco) durante esta pasada.

- 2026-04-11: Se corrigió una falsa invalidez de la media por secuencia en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) que dejaba el preview pegado al `video-preview/?video_id=...` aunque `master/proxy` ya estuvieran `ready`. La causa raíz era un desajuste de serialización en la firma `source_signature`: backend la genera con claves JSON ordenadas (`sequence_order`, `source_in_ms`, `source_out_ms`) y el frontend la construía en otro orden, así que la comparación por string siempre marcaba `stale` la media preparada. Resultado visible: loop de restauración en `handleSyncSequenceClipPreparationState(...)`, warning `Maximum update depth exceeded` y handoff fallido al `proxy.mp4`. Ahora la firma frontend quedó canonizada con el mismo orden que backend, lo que vuelve a habilitar la restauración `ready` y el uso del preview preparado. Durante la validación apareció además un `null.label` en [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js); se endurecieron los helpers de identidad (`resolveSequenceIdentity`, `resolveSequenceDisplayName`, `resolveSequenceMediaKey`, metadata editorial) para tolerar `null` transitorio sin tumbar el editor.

- 2026-04-11: Se terminó de separar la identidad técnica de la identidad operativa de secuencia también en la capa de backend/media. En [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/DashboardPage.jsx) quedó `resolveSequenceMediaKey(...)`: cuando ya existe un `mediaKey` persistido se respeta tal cual por compatibilidad, pero las secuencias nuevas pasan a derivar su carpeta de media desde `sequenceCode` (`media-seq-...`) en vez de colgar del id legacy. En [backend/apps/editor/services.py](backend/apps/editor/services.py), [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/deep_analysis.py](backend/apps/editor/deep_analysis.py) se añadió una resolución común de `display_name / sequence_code / media_key`, que ahora se usa para nombres batch (`SEQ-XXXX_Titulo.mp4`), metadata de jobs, sidecar TXT de export y respuestas de análisis profundo. Resultado: export/report/media derivada ya no vuelven a mezclar `label`, `mainTitle` e id técnico como si fueran la misma cosa. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y `manage.py check` correcto en `backend`.

- 2026-04-11: Se introdujo una capa explícita de identidad de secuencia pensando a futuro en [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js), [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js), [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) y [frontend/src/components/ProjectDeepAnalysisSequencePanel.jsx](frontend/src/components/ProjectDeepAnalysisSequencePanel.jsx). Ahora el draft persiste `title`, `sequenceCode` y `approvalState`, existen helpers únicos (`resolveSequenceIdentity`, `resolveSequenceDisplayName`, `resolveSequenceCode`, `isApprovedSequence`) y las secuencias nuevas guardadas por aprobación editorial ya no nacen con ids `editorial-approved-*`, sino con ids neutrales `seq-*`; los ids legacy siguen siendo compatibles porque la lógica de negocio dejó de inferir estado desde el prefijo del id. En la misma pasada, la preparación de media por secuencia dejó de depender de `String(sequenceId).includes("editorial-approved")` y las superficies principales del UI (tabs, cards, export batch, análisis profundo, pills/resúmenes del editor) dejaron de mostrar `label` crudo como nombre operativo. Validación final: `get_errors` limpio en los archivos tocados y `npm run build` correcto en `frontend`.

- 2026-04-11: Se corrigió otro borde real del pipeline de media por secuencia en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La mezanina no arrancaba automáticamente al abrir una secuencia aprobada por URL directa (`?project=<id>&sequence=<id>`) porque `handleTriggerSequenceClipPreparation(...)` solo se invocaba desde el click manual `handleOpenSequenceInClassicEditor(...)`; además el payload se construía desde `sequence.blocks`, pero las secuencias persistidas viven en `sourceClips` o deben derivarse con `buildSequenceSourceClips(...)`. Ahora el auto-trigger corre también cuando el editor se hidrata directamente sobre una secuencia activa y el POST usa la fuente correcta de bloques/rangos. Validación real en `http://127.0.0.1:5173/?project=25&sequence=editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1`: el toolbar pasó a mostrar `Secuencia · 74%` sin click adicional, confirmando que la preparación de `master/proxy` arrancó sola al entrar.

- 2026-04-11: Se terminó de validar con una corrida real el nuevo pipeline de media por secuencia y apareció una causa raíz concreta en [backend/apps/editor/views.py](backend/apps/editor/views.py). `_build_sequence_media_concat_command(...)` estaba armando el filtergraph de FFmpeg para `concat` como `v0 v1 ... a0 a1 ...`, lo que disparaba `Media type mismatch`; ahora los pads se envían intercalados `v0 a0 v1 a1 ...`, que es el contrato correcto del filtro. Smoke test real sobre `project=25` / `sequence=editorial-approved-1775353832995-thematic-7-50266` usando los `sourceClips` persistidos de la secuencia: `POST /sequence-media-preparation/ -> 202`, polling final `status = ready` con `master.mp4` y `proxy.mp4` generados bajo `backend/media/timeline/project_25/sequence_media/editorial-approved-1775353832995-thematic-7-50266/`; luego `POST /sequence-stabilization/ -> 202` y cierre `status = ready` con `stabilized.mp4` listo en la misma carpeta. Esto deja validado que el contrato nuevo por secuencia ya produce `master + proxy + stabilized` con un `media_key` estable y no solo a nivel de wiring/UI.

- 2026-04-11: Se perfiló el material real del proyecto 25 antes de tocar otra vez el codec del estabilizador y el hallazgo cambió el diagnóstico. Las fuentes multipart (`part_1`, `part_2`) ya son `H.264 High + AAC`, `1920x1080`, `yuv420p`, ~`60.002 fps`; el clip estabilizado viejo del bloque `block-editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1:90921` salía también en `H.264/yuv420p`, pero forzado a `30 fps` por metadata stale del `VideoAsset` (`video 29` guardado como `30/1` aunque el archivo real es ~60 fps). En [backend/apps/editor/views.py](backend/apps/editor/views.py) el worker de `clip-stabilization` ahora resuelve el frame rate desde `probe_video_metadata(source_path)` antes de armar `-r` y el GOP, en vez de confiar ciegamente en `fps_num/fps_den` persistidos. Validación real: la nueva corrida `6de7097a-0d8c-415c-81b5-caa984f67d48` volvió a `ready`, el MP4 resultante quedó en `24961/416` (~`60.002 fps`), `H.264 High`, `yuv420p`, sin B-frames y con decodificación limpia. Hallazgo adicional de entorno: en `media/videos/preview/project_25/` había un `video_28_seekable.<uuid>.tmp.mp4` corrupto (`moov atom not found`), señal de preview seekable interrumpido, pero no fue la causa directa del glitch del estabilizador porque el worker ya cae al source real cuando el preview canónico no existe.

- 2026-04-11: Se corrigió un fallo real del estabilizador en secuencias que caían sobre partes posteriores de un proyecto multi-source. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el worker de `clip-stabilization` ya no asume siempre el `active source` del proyecto: ahora resuelve la parte fuente correcta a partir de `source_in_ms/source_out_ms` globales y convierte esos offsets a tiempos locales antes de lanzar `ffmpeg vidstab`. Esto corrige el caso reproducible de `project=25` en la secuencia `editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1`, donde el backend viejo intentaba estabilizar `block ...:90921` contra `part_1` con `-ss 2199.855` y terminaba en `Reintentar estabilizador`; tras limpiar instancias `runserver --noreload` duplicadas en Windows y dejar una sola viva, la misma corrida pasó a usar `part_2` con offset local `401.220`, cerró en `status = ready` y el stream respondió `HEAD 200 video/mp4`.

- 2026-04-11: Se encontró y corrigió otra instancia de la misma familia de bug en la resolución de fuentes multipart. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `_resolve_export_clip_source(...)` ahora re-resuelve la parte correcta a partir de `source_in_ms/source_out_ms` globales antes de asumir el `source_video` heredado del draft. Esto evita que operaciones que sintetizan `DraftExportClip` con el `active source` equivocado fallen injustamente en proyectos con varias partes. Validación real en `project=25`: la secuencia `editorial-approved-1775805026485-thematic-20-85848-split-2-sub-split-1` pasaba `Audio Fix` de `error = Hay un clip que cruza dos partes fuente...` a `status = ready`, con WAV generado en `audio/enhanced/project_25/GX3-Pro-precios-y-equipo-c2025e0e-10b4-4c6d-aeb0-51f482fc16bd.wav`.

- 2026-04-11: Se simplificó de nuevo el chrome final de `Audio Fix` en el toolbar del editor clásico. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el estado listo ya no cambia el botón a `Audio fix listo` ni muestra variantes tipo `Rehacer Audio Fix`; el botón queda siempre como `Audio Fix` y, cuando ya existe WAV procesado, a su lado aparece un único toggle compacto `On/Off` para activar o desactivar rápidamente el audio procesado en preview. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se eliminó el wrapper visual extra del switch anterior y se dejó un botón cuadrado mínimo, consistente con el lenguaje recto del editor clásico. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y verificación visual en navegador del toolbar con la secuencia `Original | Estabilizado | Audio Fix | ON | Exportar`.

- 2026-04-11: Se compactó la UX de `Audio Fix` en el editor clásico para que el control sea más usable durante comparación A/B real. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el submenú dejó de envolver sliders y checkboxes dentro de una sola `label`, y pasó a filas compactas con toggle separado, slider dedicado y lectura de valor visible (`55%`, `-16 LUFS`, etc.), lo que evita la fricción al editar parámetros. En la misma pasada el toolbar reemplazó `Original / Fix` por un switch de monitor más explícito `Fix Off / On`, y se retiraron pills redundantes de estado para despejar chrome horizontal. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el menú se rehízo con gramática visual más recta, menos radio, más ancho útil para slider y menor consumo vertical por fila, alineándolo con el resto del editor clásico. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend`, snapshot del toolbar con switch `Fix Off / On` visible y prueba interactiva en navegador confirmando que un slider dentro de `Controles de Audio Fix` acepta cambios directos (`0.8`) sin perder foco ni cerrar el menú.

- 2026-04-11: `Audio Fix` del editor clásico dejó de ser solo un job/export y pasó a controlar también la fuente de audio del preview. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el botón se convirtió en un submenú persistido por secuencia con checkboxes y sliders para ruido, realce de voz, compresión, de-esser, nivelado, limiter, loudness objetivo y una bandera `Usar WAV en el preview al terminar`. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el worker `sequence-audio-enhancement` ahora acepta esos settings, los normaliza y los traduce a una cadena FFmpeg configurable en vez de usar solo un preset fijo. En el preview, cuando el WAV queda `ready` y la opción está activa, el editor mantiene el video como fuente visual pero muta el `<video>` principal y sincroniza un `<audio>` oculto con el WAV procesado a nivel de playhead/transporte; el toolbar además expone el pill `Preview fix` para dejar claro que la reproducción ya no está tomando el audio del MP4 fuente. Validación final: `get_errors` limpio en frontend/backend, `npm run build` correcto en `frontend`, `manage.py check` correcto en `backend` y smoke test real en navegador sobre `project=25` / `sequence=editorial-approved-1775353832995-thematic-7-50266`, donde el submenú mostró `8` checkboxes + `6` sliders, `Audio Fix` terminó en `ready`, el `<audio.workflow-preview-hidden-audio>` quedó montado con el WAV generado y en reproducción el estado observado fue `audioPaused=false` + `videoMuted=true`.

- 2026-04-11: El estabilizador del editor clásico pasó de ser una base opaca de `Crop` a una fuente real de reproducción de secuencia. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el preview ahora expone un toggle `Original / Estabilizado`, mantiene `src` independientes por slot del doble buffer A/B y usa resolución por bloque para que el player principal pueda cambiar entre video fuente y `clip-stabilization-stream` sin romper el handoff entre clips. En [backend/apps/editor/views.py](backend/apps/editor/views.py) se cerraron además dos causas raíz que seguían haciendo fallar esa reproducción aunque la UI ya apuntara al stream correcto: (1) `ProjectClipStabilizationStreamView` ahora puede recuperar MP4 estabilizados persistidos desde `media/timeline/project_<id>/stabilized/` aunque el backend haya reiniciado y el run ya no esté en memoria; (2) la renderización del clip estabilizado dejó de usar `mpeg4` y pasó a `libx264 + yuv420p`, corrigiendo el `DEMUXER_ERROR_NO_SUPPORTED_STREAMS` que Chrome devolvía al intentar reproducir los MP4 viejos. Validación final: `get_errors` limpio en frontend/backend, `npm run build` correcto en `frontend`, stream recuperado con `HEAD 200 video/mp4`, codec confirmado con `ffprobe` (`h264 / High / yuv420p`) sobre la nueva corrida `3dd41c53-017f-4036-babf-9f4ba4f12e84`, y smoke test en navegador sobre `project=25` / `sequence=editorial-approved-1775353832995-thematic-7-50266` donde el toggle cambió los `currentSrc` al stream estabilizado y el preview reprodujo en modo `Pause` con avance real de `0.016s -> 1.855s`.

- 2026-04-11: Validación final del estabilizador en el proyecto 25: con el backend arrancado sin autoreload por defecto, la misma corrida que antes caía a `Reintentar estabilizador` ya no se interrumpe y el bloque pendiente termina en `ready`. La reproducción real del caso mostró `POST /clip-stabilization/ -> 202`, polling estable en `200` durante todo el job y cierre final con `status = ready`, `progress = 100` y stream listo para `block-editorial-approved-1775353832995-thematic-7-50266:87328`. Hallazgo operativo adicional: la etapa `Renderizando clip estabilizado...` puede quedarse bastante tiempo visible con `68%` aunque el proceso siga sano; en esta validación tardó alrededor de 75 segundos en completar, así que ese `68%` es hoy un marcador de etapa, no evidencia de cuelgue por sí mismo.

- 2026-04-11: El editor clásico ganó `Audio Fix` a nivel secuencia dentro del toolbar del preview. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js), [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) y [frontend/src/styles/app.css](frontend/src/styles/app.css) ahora existe un job de `Audio Fix` con progreso/cancelación, persistido por secuencia dentro del draft y con acceso rápido al WAV generado. En [backend/apps/editor/views.py](backend/apps/editor/views.py), [backend/apps/editor/urls.py](backend/apps/editor/urls.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) se añadió el worker `sequence-audio-enhancement` sobre la secuencia activa: concatena el rango completo del timeline, aplica una cadena conservadora de limpieza/nivelado con FFmpeg (`highpass`, denoise, compresión, limitador, loudness) y guarda un WAV derivado en `media/audio/enhanced/project_<id>/...`. El export de video ahora detecta automáticamente ese asset y, si la firma de los clips sigue coincidiendo con la secuencia actual, usa el audio mejorado en vez del audio concatenado original. Validación final: `manage.py check` correcto en `backend` y `npm run build` correcto en `frontend`.

- 2026-04-11: Se corrigió otra fuente de confusión alrededor del estabilizador del preview: el job sí podía completar sin errores, pero el toolbar seguía sugiriendo una acción sobre el preview principal cuando en realidad la salida estabilizada se estaba usando sobre todo como base para `Crop/tracking`. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el estado `ready` ahora expone una acción visible `Ver base` que abre `Crop` sobre el clip estabilizado del bloque actual, y el botón principal pasa a leerse como `Rehacer estabilizador` cuando la secuencia ya está lista, evitando la falsa expectativa de que `Estabilizado` iba a cambiar inmediatamente el preview principal por sí solo. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-11: Se confirmó que una parte del reporte `el estabilizador no funciona` seguía siendo entorno, no lógica del botón. La captura mostraba `ERR_CONNECTION_REFUSED` directo contra `http://127.0.0.1:5173/api/...`, lo que significa que el request ni siquiera estaba llegando al backend: el frontend Vite estaba caído. Durante la verificación también apareció una segunda causa real en [start-backend.ps1](start-backend.ps1): el launcher PowerShell del backend tenía una sintaxis inválida (`[int](if (...) ...)`) y no podía levantar Django desde esa ruta. Ahora ese script resuelve primero `VIDEO_EDITOR_BACKEND_PORT` y luego hace el cast correcto, de modo que `start-backend.ps1` vuelve a arrancar `runserver`. Validación final: frontend vivo en `127.0.0.1:5173`, backend vivo en `127.0.0.1:8000`, la secuencia volvió a abrir en el editor y `POST /api/editor/projects/25/clip-stabilization/` respondió `202` para bloques pendientes reales de la secuencia activa.

- 2026-04-11: El toolbar `Estabilizador-on` del preview dejó de correr una estabilización aislada y pasó a orquestar la secuencia activa bloque por bloque. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la corrida ahora usa los endpoints existentes por bloque para procesar todos los clips elegibles de la secuencia, con progreso global, cancelación del bloque activo y estado parcial (`Continuar estabilizador`, `Bases X/Y`) cuando solo parte de la secuencia quedó lista; en [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) el botón y sus pills ya reflejan ese estado secuencial en vez de venderlo como una operación invisible sobre un solo clip. En la misma pasada [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js) dejó de descartar keys de nivel bloque al guardar buckets por aspecto, corrigiendo una pérdida silenciosa por la que `stabilization` podía borrarse después de editar `Crop` o `displayMode` por aspecto. Validación final: `get_errors` limpio en los archivos tocados y `npm run build` correcto en `frontend`.

- 2026-04-10: Se corrigió el origen real de una cascada de errores que hacía parecer roto el nuevo `Estabilizador-on`. El problema no estaba en el job de estabilización sino en el entorno de desarrollo: [frontend/src/main.jsx](frontend/src/main.jsx) ya fuerza `localhost -> 127.0.0.1` para unificar `localStorage`, pero [frontend/vite.config.js](frontend/vite.config.js) todavía podía arrancar Vite escuchando solo en `localhost` cuando se usaba `npm run dev` directo. Resultado: el navegador se redirigía a `127.0.0.1:5173`, el frontend quedaba inaccesible y aparecían errores en cascada (`DashboardPage.jsx 500`, websockets caídos, `ERR_CONNECTION_REFUSED` y luego fallos aparentes del estabilizador). Ahora Vite fija `server.host = 127.0.0.1` por defecto, de modo que el arranque manual y los scripts del repo quedan alineados con la URL canónica. Validación final: `http://127.0.0.1:5173/` responde `200`, `npm run build` correcto en `frontend`, `clip-stabilization` probado sobre el proyecto 25 con una corrida corta que llegó a `ready` y stream `video/mp4` válido.

- 2026-04-11: El editor clásico ganó un flujo operativo de estabilización por clip, ya no solo estabilización interna para análisis de `auto frame`. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) se añadieron endpoints en memoria para iniciar, consultar, cancelar y streamear una corrida `ffmpeg vidstab` por bloque (`clip-stabilization`, `clip-stabilization-status`, `clip-stabilization-control`, `clip-stabilization-stream`). En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) apareció el botón `Estabilizador-on` dentro del toolbar del preview: mientras corre se convierte en una UI inline con porcentaje, barra y `X` de cancelación; cuando termina, el estado `ready` se persiste en `clipDisplayAdjustments[blockId].stabilization` y el `Crop` pasa a abrir sobre el clip estabilizado resultante como nueva base para tracking manual. Validación final: `get_errors` limpio, `npm run build` correcto en `frontend` y `manage.py check` correcto en `backend`.

- 2026-04-11: El backend de `auto frame` ahora puede estabilizar de forma conservadora el material antes de muestrear frames para POI. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) se añadió una pasada opcional con `ffmpeg vidstabdetect + vidstabtransform` por bloque (`local-autoframe-v4`), limitada a clips de duración moderada y solo cuando el bloque cae dentro de una sola parte fuente; si no, el motor cae al flujo anterior sin romper el análisis. La evidencia del bloque ahora deja trazado si la estabilización se aplicó, con qué modo y por qué motivo se omitió o falló. Validación final: `get_errors` limpio y `manage.py check` correcto.

- 2026-04-11: El modal `Crop` del editor clásico ganó una primera superficie explícita para enseñar seguimiento al sistema desde la propia UI. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) ahora existen botones `Face tracking` y `Object tracking`, un rectángulo objetivo independiente del `cropRect` que puede dibujarse/moverse/redimensionarse sobre el frame, y un slider de `intensidad de seguimiento` persistido por clip/aspecto dentro de `clipDisplayAdjustments`. En la pasada más reciente esa persistencia se endureció para aprendizaje editorial: cada muestra manual guarda además una `learningSample` explícita con `captureIntent`, instrucción de recreación, timestamps, `cropRectAtCapture`, `targetCenter`, `targetSize`, modo, intensidad y contexto del aspecto/modo vigente, de modo que futuras rutinas de análisis puedan entender no solo dónde estaba la caja sino por qué y cómo debería recrearse. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-11: Se añadió una función explícita de tracking con inercia para POI en el backend de `auto frame`. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) `_track_boxes_with_inertia(...)` ahora suaviza el centro y el tamaño de las cajas detectadas entre samples con limites de paso por frame (`max_center_step`, `max_size_step`) en vez de depender solo de la union bruta de boxes. Esa función ya se usa para construir `primary_track_box`, `pair_left_track_box`, `pair_right_track_box` y el `context_box`, de modo que el centrado del encuadre pueda seguir caras o zonas con cierta inercia y menos saltos laterales. Validacion final: `get_errors` limpio en el archivo y `manage.py check` correcto.

- 2026-04-11: `Encuadres` dejo de depender de haber corrido `Auto frame` para existir. En [frontend/src/components/AutoFrameTranscriptPanel.jsx](frontend/src/components/AutoFrameTranscriptPanel.jsx), [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el panel ahora muestra siempre el catalogo visual de todos los clips disponibles de la secuencia usando el encuadre base actual del editor, y el ultimo run de `Auto frame` pasa a ser solo una capa de estado sobre esos mismos cards (`Auto` vs `Base`, cambios por aspecto, resumen del ultimo run). En la misma pasada las miniaturas quedaron aun mas compactas y el click sobre la foto abre directamente el `Crop` del clip real para permitir una pasada rapida de reframing manual desde `Encuadres`. Validacion final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-11: Se afinó la fidelidad visual de `Encuadres` para que los clips `fit` dejen de parecer una tira del source y se lean como el preview real del editor. En [frontend/src/components/AutoFrameAspectThumbnailStrip.jsx](frontend/src/components/AutoFrameAspectThumbnailStrip.jsx) el renderer de thumbnails `fit` ahora monta un backdrop blur usando la misma logica de `cover` del preview, pero conserva delante un frame `fit` mas pequeno (`FIT_PREVIEW_PRESENTATION_SCALE = 0.82`) y centrado sobre un viewport editorial en vez de usar la forma cruda del `cropRect`/POI como formato final. El frame interior ahora cuantiza el encuadre a formatos utiles (`16:9`, `4:3`, `1:1` o `9:16`) segun la geometria del POI y luego construye un viewport centrado que lo contiene; en [frontend/src/styles/app.css](frontend/src/styles/app.css) se anadieron las capas `.workflow-auto-frame-thumbnail-backdrop*` y se endurecio el frame interior para que el resultado se lea como `preview con fondo blur + cuadro activo`, no como banda negra. Validacion final: `get_errors` limpio, `npm run build` correcto en `frontend` y verificacion DOM en `5173` sobre `project=25`, donde un card `V fit · recorte ajustado` paso a medir `frameAspectRatio = 1.778` (`16:9`) con backdrop blur activo.

- 2026-04-11: Se corrigio la heuristica de `auto frame` en `portrait` para que el motor deje de conservar `fit` con pillarbox cuando ya existe un POI humano claro dentro de un source `16:9`, y ademas pueda sesgar el recorte hacia el lado activo del dialogo aunque la segunda cara no entre limpia en Haar. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) el motor paso a `local-autoframe-v3`: se anadio la regla `portrait-poi-single`, una pasada de contexto que promueve `V fit -> fill` cuando hay presencia facial estable y, como fallback, una lectura de energia de movimiento entre samples para desplazar el POI horizontal cuando la actividad dominante cae en el lado opuesto al rostro detectado. Validacion final: `get_errors` limpio, `manage.py check` correcto y prueba real en `http://127.0.0.1:5173/?project=25&sequence=editorial-review-1775867161445-thematic-20-85848-split-1-split-3`, donde `Auto frame` desde la propia UI dejo `V Fill 6 · Fit 2 · Split 2`, el clip `contamos con 4 años o 100,000,000 km` quedo en `V fill · recorte ajustado` y `representado por Excel aca en Guatemala` mantuvo `V fill` pero movio su `cropRect` portrait de `x=0.4168` a `x=0.2583`, desplazando el encuadre hacia la izquierda.

- 2026-04-11: Se movio la revision visible de `auto frame` fuera del canvas del preview y hacia el panel de transcript, para que el resultado sea inspeccionable sin ensuciar la superficie principal de visionado. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), [frontend/src/components/AutoFrameTranscriptPanel.jsx](frontend/src/components/AutoFrameTranscriptPanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el panel izquierdo ahora ofrece pestañas `Transcript` / `Encuadres`; `Encuadres` muestra el ultimo run con thumbnails reales por clip, POI estimado, razones del motor y detalle seleccionable, mientras el preview queda limpio sin la tarjeta inline bajo el toolbar. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) se ajusto ademas el crop para `fill` y `split`: el tamano base sale del `primary_box` / caja del sujeto y el `track_box` solo aporta un margen de movimiento acotado, de modo que el encuadre responda mas al POI que a la union completa del desplazamiento. Validacion final: `get_errors` limpio, `npm run build` correcto en `frontend`, `manage.py check` correcto en `backend` y prueba real en navegador sobre `project=25` / `sequence=editorial-review-1775867161445-thematic-20-85848-split-1-split-3`, donde `Encuadres` quedo visible con `10` tarjetas con thumbnail y el preview confirmo `0` tarjetas inline de impacto.

- 2026-04-11: Se endureció la heurística del motor local de `auto frame` para alinearla mejor con cortes de diálogo y talking heads en `16:9`. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) el motor pasó a `local-autoframe-v2`: ahora evita dejar en `fit` los clips landscape con sujeto humano claro, usa hints de speakers del transcript cuando existen para reforzar decisiones de diálogo, calcula cajas de seguimiento más seguras a partir de varios samples del clip y aplica una pasada secuencial para alternar `split`/`fill` cuando dos cortes consecutivos pedirían el mismo split. Validación final: `manage.py check` correcto; prueba dirigida por snippet Python sobre `project=25` / `sequence=editorial-review-1775867161445-thematic-20-85848-split-1-split-3` devolvió `10/10 fill` en landscape para la secuencia persistida; y prueba manual en la UI real del mismo review sequence mostró `10 clips analizados · 9 con cambio en H · 1 solo en otros aspectos`, con la mayoría de clips pasando de `H fit → fill`.

- 2026-04-10: Se afinó el resumen visible de `auto frame` para evitar falsos positivos de lectura cuando un clip cambia en otro aspecto pero no en el aspecto actual del preview. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el bloque `Último auto frame` ahora separa `cambio en H/V/1:1 actual` de `cambio solo en otros aspectos`, tanto en el contador general como en cada clip. Validación manual sobre `project=25` / `sequence=editorial-review-1775867161445-thematic-20-85848-split-1-split-3`: el run real terminó correcto y el resumen mostró `10 clips analizados · 0 con cambio en H · 10 sin cambio`, que coincide con el resultado esperado del motor para esa secuencia.

- 2026-04-10: Se hizo visible el efecto real de `auto frame` para evitar la sensación de `no hizo nada`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el editor clásico ahora guarda un resumen del último run con diff por clip: cuántos clips se analizaron, cuántos tuvieron cambio visible y, para cada clip, el estado `antes → después` por aspecto. Ese resumen vive justo debajo del toolbar del preview, no escondido en `Más opciones`, y cada item es clickable para saltar al clip correspondiente. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-10: Se añadió la segunda capa de UX para `auto frame` en el editor clásico, enfocada en no pisar ajustes manuales. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) y [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) la secuencia ahora persiste `manualClipDisplayBlockIds`: cualquier cambio manual de `modo` o `crop/adjustment` marca el clip como override manual, y esa marca se conserva o limpia coherentemente al dividir, fusionar, borrar o reaplicar auto frame. En [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) quedó un modal de rerun con tres caminos: reanalizar solo clips intactos, sobrescribir toda la secuencia o elegir clips concretos. En [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx), [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el estado manual se muestra en naranja tanto en preview como en timeline. Validación final: `get_errors` limpio y `npm run build` correcto en `frontend`.

- 2026-04-10: Se añadió `auto frame` local para el editor clásico, aplicado sobre todos los clips de la secuencia activa en una sola corrida. En [backend/apps/editor/auto_frame.py](backend/apps/editor/auto_frame.py) se implementó el motor local con OpenCV + FFmpeg: muestrea varios frames por clip, detecta caras, construye sujetos aproximados y decide `fill`, `fit` o `split` por aspecto (`landscape`, `portrait`, `square`) con reglas explícitas de dominancia, paridad entre sujetos, separación y preservación de contexto; además devuelve `clip_display_adjustments` listos para persistirse como `cropRect/cropPreset` usando el mismo contrato del preview/export. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) quedó expuesto `POST /api/editor/projects/<id>/auto-frame/`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el editor clásico ganó botón `Auto frame`, aplicación masiva sobre `derivedBlocks`, persistencia con undo/redo y un resumen diagnóstico por clip dentro de `Más opciones`. Validación final: `manage.py check` correcto, `npm run build` correcto y prueba funcional sobre el proyecto 25 con la secuencia activa `editorial-approved-1775353832995-thematic-7-50266`, donde el motor procesó `4/4` clips y devolvió conteos por aspecto sin errores.

- 2026-04-10: Se cerró la segunda pasada de la slide 4 para llevarla del `selector + batch` a una superficie de export más operativa y legible. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) los exports batch ahora salen bajo `exports/batch/project_<id>/`, con nombre base por secuencia (`Bloque_1.mp4` + `Bloque_1.txt`) en vez de arrastrar también el nombre del proyecto al archivo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `buildBatchExportSequenceItems(...)` quedó enriquecido con `sourceClips`, rangos de preview y apertura directa al editor clásico incluso para proyectos no activos; y en [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) cada secuencia muestra ahora thumbnail filmstrip con la misma lógica hover de [frontend/src/components/SuggestionThumbnailStrip.jsx](frontend/src/components/SuggestionThumbnailStrip.jsx), además de un preview inline al hacer click con botones `Reproducir`, `Reiniciar`, `Editar` y `Cerrar`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se dejó nombrado internamente el profile visual actual como `lightwave-atelier` para reutilizar esta gramática minimalista en futuros themes. Validación final: `manage.py check` correcto, `POST /api/editor/projects/25/export/video/batch/` -> `202` con `folder_path = exports/batch/project_25`, `output_name = Bloque_1.mp4`, `npm run build` correcto y preview inline confirmado en navegador.

- 2026-04-10: Se terminó de cablear la slide 4 como superficie operativa de export batch por proyecto. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el panel `Batch export` ahora permite elegir proyecto, cargar el draft activo o el `editor_draft` persistido de otro proyecto, marcar secuencias con checkbox, escoger aspect ratio y lanzar exportaciones múltiples contra el endpoint batch ya existente. En [frontend/src/components/BatchProjectExportPanel.jsx](frontend/src/components/BatchProjectExportPanel.jsx) quedó visible además que cada corrida genera un MP4 y un TXT de contexto; en [frontend/src/components/ProjectExportsPanel.jsx](frontend/src/components/ProjectExportsPanel.jsx) el historial muestra también `description_sidecar_name`, y en [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) los hashtags del draft se normalizan ya a un máximo de 10 para quedar alineados con backend. Validación final: `manage.py check` correcto en `backend`, `POST /api/editor/projects/25/export/video/batch/` -> `202 Batch export iniciado para 1 secuencias.` y `npm run build` correcto en `frontend`.

- 2026-04-10: La depuración de secuencia ahora deja trazabilidad explícita del prompt usado en cada corrida para preparar análisis comparativos entre criterio IA y edición final. En [backend/apps/editor/services.py](backend/apps/editor/services.py) cada request `sequence-depuration` adjunta `prompt_metadata` con familia, origen (`bundled-default` o `workspace-custom`), versión, fecha, fingerprint y tamaño del prompt; esa metadata viaja tanto en `request_payload` persistido en `OpenAITranscriptionLog` como en `_openai_meta` del resultado. En [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx) la auditoría del modal ahora muestra versión, fecha, origen y hash del prompt para que futuras revisiones del proyecto puedan comparar una corrida concreta contra el resultado final editado y decidir cambios de prompt con trazabilidad.

- 2026-04-10: Se endureció la persistencia de las rutas que crean secuencias nuevas para reducir riesgo de pérdida de trabajo si el navegador se cae o el usuario cambia de estado antes de que corra el persist diferido. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la creación manual de secuencia ahora hace `pushUndoSnapshot()` antes de insertar, construye el nuevo draft completo y dispara un guardado crítico inmediato que escribe draft local, crea backup recuperable y persiste `editor_draft` al backend. En [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js) se aplicó la misma garantía a tres flujos: aprobar una candidata editorial, aprobar una sugerida guardada y aprobar una división en múltiples secuencias. Con esto, las secuencias recién nacidas quedan cubiertas por undo desde el primer paso y dejan rastro durable sin esperar solo al debounce general de `requestPersist()`. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se reorganizó la slide 3 para reducir la confusión entre `secuencia sugerida` y `secuencia creada`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el módulo derecho dejó de llamarse `Secuencias sugeridas` y pasó a ser un único módulo `Secuencias`, dividido en dos tramos: `Secuencias creadas` arriba y `Sugeridas` abajo. Las creadas usan ahora la misma gramática visual de card que las sugeridas, pero como estado superior y listo para edición; las sugeridas quedan como cola pendiente de revisar/aprobar. En paralelo, el módulo izquierdo de creadas se mantuvo como acceso rápido, pero cada botón ganó thumbnail pequeño reutilizando [frontend/src/components/SuggestionThumbnailStrip.jsx](frontend/src/components/SuggestionThumbnailStrip.jsx) para reconocer visualmente cada secuencia además del título. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron estilos para las nuevas secciones unificadas y para la variante compacta del thumbnail en la lista izquierda. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se corrigió la apertura por URL de secuencias inexistentes para que el editor no cambie silenciosamente a otra `editorial-approved-*`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `openProject(...)` ahora detecta cuando `preferredSequenceId` fue pedido de forma estricta (por URL directa) pero ya no existe en el draft hidratado; en ese caso abre el proyecto en `Secuencias` sin takeover al editor, limpia la secuencia inválida de la ruta en vez de sustituirla por otra y muestra un mensaje explícito de que esa secuencia ya no existe en el estado persistido actual. Durante la investigación se verificó además que `editorial-approved-1775808790654-thematic-26-22200` no está ni en `localStorage`, ni en `bootstrap.editor_draft`, ni en `backend/db.sqlite3`, por lo que el problema no era rehidratación incompleta de `depurationSession` sino ausencia real de esa secuencia en el estado persistido. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se aclaró la UX de `Depurar secuencia` cuando ya existe una corrida previa guardada. En [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx) el modal ahora explicita que está mostrando la última depuración persistida de la secuencia y diferencia dos caminos: `Usar resultado anterior` para reaprovechar la corrida previa y `Reanalizar secuencia actual` para lanzar una nueva llamada sobre el estado actual del editor. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó cableado `handleRerunDepurationFromCurrentSequence()` para relanzar el análisis sin obligar al usuario a pasar por `Rehacer desde original`. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se optimizó la depuración para reducir carga enviada y evitar esperas colgadas. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `depurate_sequence(...)` ahora deduplica `word_ids`, compacta `analysis_context.sequence_paragraph_map` (sin texto repetido), reduce ventana de contexto auxiliar a 20 palabras por lado y elimina campos redundantes por palabra (`role`) para bajar tokens sin perder precisión de `word_id`. En esa misma ruta `_request_openai_json_response(...)` quedó con `timeout` explícito y la llamada de depuración usa `request_timeout_seconds=120` para no quedar bloqueada indefinidamente. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `ProjectDepurateSequenceView.get(...)` agrega detección de corrida stale en `processing`: si supera umbral sin actualizar, pasa a `error` con mensaje claro en vez de quedar esperando para siempre. También se añadió telemetría visible en log de depuración con el tamaño real del payload enviado (`activas / contexto antes / contexto después`). Validación final: `get_errors` limpio y `manage.py check` correcto.

- 2026-04-10: Se corrigió una causa real de `duplicación aparente` en `Depurar secuencia` al reabrir el modal durante una corrida en curso. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), `handleOpenDepurateModal()` estaba limpiando `depurateRunId` y reseteando a `idle` cuando todavía no existía `depurationSession.result`; eso cortaba el polling de la corrida activa y hacía que volviera a mostrarse `Iniciar análisis`, permitiendo lanzar una segunda corrida innecesaria. Ahora el modal detecta corridas en vuelo (locales y persistidas), reanuda estado `processing` y conserva `runId`; además `handleStartDepuration()` persiste `depurationSession.status = processing` con `runId` para recuperación robusta al reabrir o cambiar de secuencia. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se corrigió un desfase de limites de clip que aparecía sobre todo al mover el hook al inicio desde depuración IA. En [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), dentro de `deriveSequenceBlocks(...)`, cuando un boundary tenía `breakAfterWordIds` y `visualSplitPoints` al mismo tiempo, el cálculo priorizaba `customSplitPoints` y podía ignorar el `sourceMs` visual; eso dejaba margen para que el bloque se estirara de más antes del siguiente corte. Ahora el boundary exacto usa `customSplit` si existe y, si no, cae a `visualSplit` en la misma frontera; además se marca ese cierre visual como boundary explícito para evitar arrastre temporal no deseado en esa transición. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: Se hizo una pasada de rendimiento enfocada en liberar RAM/GPU al salir de secuencias y cerrar proyecto. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el preview de secuencia ya no monta video cuando no hay secuencia activa (`sequencePreviewVideoUrl` queda vacío), se añadió liberación agresiva de recursos de preview (`releaseSequencePreviewVideoResources`) al cerrar secuencia y se incorporó un cierre real de proyecto (`handleCloseProjectAndReleaseResources`) desde el botón `Cerrar proyecto`, limpiando estado pesado (preview, timeline media, cache waveform, historial y selección activa) para evitar que quede trabajo residual en memoria. En el mismo archivo, `loadTimelineMedia(...)` ahora limita el cache en memoria de waveform maestro a un máximo acotado y refresca recencia para evitar crecimiento sin techo durante sesiones largas; además la persistencia de sesión redujo churn durante playback cuantizando playhead/scroll antes de guardar. En [frontend/src/pages/dashboardLocalStorageHelpers.js](frontend/src/pages/dashboardLocalStorageHelpers.js) `saveEditorSession(...)` ahora evita escrituras redundantes cuando el payload no cambió. En [frontend/src/components/SuggestionThumbnailStrip.jsx](frontend/src/components/SuggestionThumbnailStrip.jsx) se limitó también el cache en memoria de frames de miniaturas para bajar presión de RAM al navegar muchas sugeridas. Validación final: `get_errors` limpio y `npm run build` correcto.

- 2026-04-10: El `Titulo principal` del editor clásico dejó de ser un toggle plano y pasó a una herramienta persistente por secuencia. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el botón ahora abre un submenú adaptable con switch `on/off`, duración editable con spinner, texto override y cierre por click afuera / `Escape`; esos datos se guardan en el draft editorial como `mainTitleEnabled`, `mainTitleDurationMs` y `mainTitleOverrideText`. En la misma pasada [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) quedó cableado para mostrar la lane `G` arriba de `T/V/A`, con clip clickable que reabre el menú, y [backend/apps/editor/services.py](backend/apps/editor/services.py) pasó a respetar override y duración del título en el overlay ASS de export en vez de quemarlo durante toda la secuencia. Validación final: `get_errors` limpio en los archivos tocados.
- 2026-04-08: Se ajustó de nuevo la mitigación del glitch del backdrop blur en modo `fit` dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/pages/sequencePreviewVideoHelpers.js](frontend/src/pages/sequencePreviewVideoHelpers.js). La causa raíz era sutil: el blur se volvía a mostrar esperando “avance de tiempo”, pero en un seek de borde el navegador puede presentar ya el frame correcto con el mismo `mediaTime`, así que esa guardia podía vencer por timeout aunque el repaint útil todavía no estuviera confirmado. Ahora el backdrop sigue ocultándose de inmediato a nivel DOM, pero se libera usando `waitForNextPresentedVideoFrame(...)`, que espera el siguiente frame pintado aunque no haya avance de tiempo. Validación final en esta pasada: `get_errors` limpio en los archivos tocados y sin nuevos runtime errors reportados.
- 2026-04-08: Se revirtió un intento de corrección específica sobre el backdrop blur del preview clásico en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) porque empeoraba la reproducción y disparaba `Maximum update depth exceeded` en runtime. El estado estable de esta pasada conserva solo la corrección del handoff visual del clip principal; el comportamiento del blur de fondo queda pendiente de atacarse por una ruta más acotada, sin volver a mezclar la sincronización auxiliar con el transporte principal. Validación final tras la reversión: `get_errors` limpio y sin nuevos runtime errors reportados.
- 2026-04-08: Se corrigió un glitch visual del preview clásico al pasar automáticamente de clip a clip en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La causa era que el editor adelantaba el bloque visual siguiente (`active/selected/presented`) justo al detectar el fin del clip actual, antes de que `playMultipartVideo(...)` hubiese presentado el primer frame real del nuevo clip. Eso permitía que durante un instante el preview aplicara el layout/crop/modo del bloque siguiente sobre el frame anterior, produciendo el “salto raro y luego se acomoda” visible en transición. Ahora el handoff visual al siguiente bloque ocurre solo después de que el video principal confirma la reanudación del nuevo clip. Validación final en esta pasada: `get_errors` limpio y sin nuevos runtime errors reportados.
- 2026-04-08: Se corrigió el tramo más frágil del flujo `Depurar secuencia` dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): `Aplicar depuración` ahora registra la operación dentro del history real del editor (`captureHistory: true`) en vez de dejar parte del cambio fuera del stack de undo, y en esa misma transacción aplica también el `auto-move` visual del hook cuando el resultado trae un span válido. Con esto, desactivaciones, ocultado de subtítulos, correcciones de transcript, modos de display y el salto visual del hook al inicio quedan en un solo paso reversible. En la misma pasada `Mover hook al inicio` y `devolver bloque a su orden original` también quedaron cableados al historial, para que el ida/vuelta del hook ya no deje al usuario sin `Undo`. Validación final en esta pasada: `get_errors` limpio en el archivo tocado.
- 2026-04-08: El arranque rápido del backend dejó de estar amarrado a `8000`. En [start-backend.ps1](start-backend.ps1) y [start-backend.cmd](start-backend.cmd) el puerto ahora puede definirse con `VIDEO_EDITOR_BACKEND_PORT`, manteniendo `8000` como default. También se documentó en [README.md](README.md) cómo levantar Django en otro puerto y cómo alinear Vite mediante `VITE_API_BASE_URL` cuando el frontend debe proxyar hacia ese backend alterno.
- 2026-04-08: La hidratación por URL volvió a tomar el camino correcto para links compartidos con secuencia explícita. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el arranque desde `?project=<id>&sequence=<sequenceId>` ya no depende de que la sesión guardada previa estuviera exactamente en takeover; ahora cualquier link que traiga `sequence` abre directamente el editor clásico sobre esa secuencia, en vez de quedarse en el shell de biblioteca/slide 1. Esto corrige el caso reproducible del proyecto 25 con secuencias `editorial-approved-*` abiertas por URL directa. Validación final en esta pasada: `get_errors` limpio en el archivo tocado.
- 2026-04-08: Se endureció el contrato de `Depurar secuencia` para que la IA trabaje solo sobre texto activo y no vuelva a tocar criterio editorial ya fijado por apagado manual. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el POST a depuración ahora filtra `sequence.wordIds` contra `inactiveWordIds`, de modo que al backend solo viajan las palabras activas; si la secuencia quedó completamente apagada, el flujo corta con error explícito en vez de mandar basura. En [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx) el copy inicial se actualizó para dejar claro que las palabras ya apagadas no se envían al modelo, que el hook se busca únicamente dentro del contenido activo y que el modal muestra una vista previa del texto exacto que se enviará, junto con el contexto antes/después que también viaja como apoyo. En [backend/apps/editor/services.py](backend/apps/editor/services.py) se reforzó además el `instructions` enviado a OpenAI y, cuando el modelo devuelve `hook_start_word_id`/`hook_end_word_id` válidos, `hook_text` se reconstruye desde ese span real de `sequence_words` para que el hook mostrado quede anclado a palabras activas de la secuencia y no a texto libre hallucinado. Validación final en esta pasada: `get_errors` limpio en los archivos tocados.
- 2026-04-08: Se afinó además la UX del caso `0 palabras activas` en `Depurar secuencia`. Antes, al pulsar `Iniciar análisis`, el modal podía pasar a una vista de error aunque en realidad no se hubiera enviado ninguna solicitud. Ahora [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) mantiene el modal en estado idle si la secuencia no tiene palabras activas y solo publica un mensaje de estado del editor; en paralelo [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx) muestra el conteo `total / activas / apagadas`, avisa explícitamente que el backend no recibirá ninguna petición en ese estado y deja deshabilitado el botón `Iniciar análisis`. Validación final en esta pasada: `get_errors` limpio en los archivos tocados.
- 2026-04-08: La depuración de secuencia dejó de ser un flujo desechable y pasó a persistir su auditoría por secuencia dentro del draft del editor. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx), [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), [frontend/src/pages/dashboardSequenceTimelineHelpers.js](frontend/src/pages/dashboardSequenceTimelineHelpers.js), [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) y [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js) la secuencia ahora guarda `depurationSession` y `visualBlockOrderIds`: si ya se depuró una vez, al abrir `Depurar` reaparece directamente el modal con el log, trace, request/response y resultado previos en vez de disparar otra llamada. Si esa corrida ya fue aplicada, el botón principal queda como `OK` y solo cierra el modal sin volver a tocar el transcript. Además, `Rehacer desde original` restaura primero la secuencia al snapshot original y solo después vuelve a lanzar la depuración. En la misma pasada `Mover hook al inicio` dejó de reordenar `wordIds`: ahora corta y reordena solo el orden visual de bloques, de modo que preview/audio/subtítulos/export pueden arrancar con el hook mientras el transcript vertical conserva sus palabras en la posición original; el bloque movido sigue marcado con el naranja de reorder dentro del transcript. Validación final de esta pasada: `get_errors` limpio y `npm run build` correcto.
- 2026-04-09: Se añadió un `corte solo visual` al editor clásico para desacoplar el layout del clip de la semántica del transcript. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `Shift` ahora activa una línea azul en timeline y `Shift + C` crea un boundary independiente que no toca `breakAfterWordIds`, duración textual ni segmentación del transcript; solo parte el clip a nivel visual y clona al bloque derecho el `clipDisplayMode` / `clipDisplayAdjustment` vigente para que desde ahí pueda cambiarse a `fit`, `fill` o `split` sin afectar el resto. En [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), [frontend/src/pages/dashboardSequenceTimelineHelpers.js](frontend/src/pages/dashboardSequenceTimelineHelpers.js), [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js), [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js), [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadió soporte persistente para `visualSplitPoints`, propagación en clones/remapeos y render del playhead azul. Validación final de esta pasada: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: Se corrigió en [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js) la deriva del bloque derecho después de un `custom trim cut` exacto (`Ctrl/Cmd` + línea roja + `C`) en el editor clásico. La causa raíz era que `handleSplitAtPlayheadWithMode(true)` sí persistía el `sourceMs` exacto del corte en `customSplitPoints`, pero `deriveSequenceBlocks(...)` solo aplicaba ese boundary al `source_out_ms` del bloque izquierdo; al crear el bloque derecho volvía a arrancar en `word.start_ms` de la primera palabra activa. Resultado visible: el clip derecho “se tragaba” el espacio detectado hacia el inicio de esa palabra en vez de respetar el punto temporal exacto del corte. Ahora, cuando un bloque nace después de un `breakAfter` con `customSplitPoint`, su `source_in_ms` también arranca en ese mismo boundary exacto, de modo que ambos lados comparten el corte preciso sin recolapsar el gap. Validación final en esta pasada: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: El refresh del dashboard dejó de depender solo de inferencias débiles sobre `activeView/editorTakeoverMode` y pasó a persistir un estado explícito de `secuencia abierta` en [frontend/src/pages/dashboardLocalStorageHelpers.js](frontend/src/pages/dashboardLocalStorageHelpers.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La sesión ahora guarda si existe una única secuencia activa abierta, en qué superficie quedó abierta (`editor` clásico vs `library` shell) y desde qué slide debe rehidratarse. Durante la hidratación, tanto con URL parcial como sin URL, ese estado tiene prioridad sobre el fallback al slide 1: si la bandera está activa y la secuencia todavía existe en el proyecto, la apertura vuelve directo a esa secuencia y a su misma superficie, con playhead/bloque/zoom/scroll/chrome restaurados. La solución sigue siendo local por cliente/browser, así que no introduce estado global compartido entre usuarios del mismo servidor; queda preparada para convivir con un escenario multiusuario futuro sin asumir sesión única del backend. Validación final en esta pasada: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: Se corrigió en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la mezcla conceptual entre `crop` y `ajuste` del preview en modo `fill`. Hasta ahora el preview aplicaba `cropRect`, `scale` y `offset` dentro de la misma transformación del `<video>`, así que achicar el crop daba la sensación de “solo reescalar el video dentro del recorte” y el contorno de ajuste quedaba anclado al canvas/frame base en vez de seguir al resultado manipulado. Ahora el preview separa las capas: `cropRect` define únicamente la ventana fuente y el zoom derivado del recorte, mientras que `scale/offset` mueven y escalan después el frame interno resultante dentro de un viewport externo que hace de límite. En paralelo, [frontend/src/styles/app.css](frontend/src/styles/app.css) hace que `mode-fill` recorte el overflow del viewport externo para que el clip transformado quede acotado por el canvas final. Validación final en esta pasada: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: Se corrigió otra regresión directa de modularización en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) que dejaba aparentemente “muerto” el modal de crop. El wiring visual del modal seguía presente y los handlers de `pointerdown` sí se disparaban, pero la rutina global que procesa el drag terminaba en `ReferenceError: updateSelectedClipAdjustment is not defined`, así que ni mover el rectángulo ni redimensionarlo podían aplicar cambios. Se restauró `updateSelectedClipAdjustment(...)` en el runtime activo, con soporte para buckets por aspecto y panel split, que es la misma pieza usada tanto por crop como por los ajustes de preview. Validación final en esta pasada: `get_errors` limpio en el archivo tocado y build de frontend pendiente tras aplicar el parche.
- 2026-04-08: Se restauraron en el runtime activo de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) las definiciones de `applyPreciseBlockTrim(...)` y `applyBlockTrim(...)`, que habían quedado ausentes aunque la timeline seguía invocándolas durante el drag de handles. En la misma pasada el trim preciso dejó de clampsear contra los bloques vecinos derivados del transcript y pasó a respetar solo los límites reales del proyecto, de modo que `Ctrl/Cmd + drag` puede extender manualmente el clip hacia adelante o hacia atrás aunque eso invada el rango cronológico que antes imponía la segmentación automática. También se endureció la rehidratación por URL/sesión en ese mismo archivo: si el usuario refresca dentro del editor clásico y el proyecto coincide, la apertura reutiliza la secuencia, playhead, bloque seleccionado, zoom/scroll de timeline y chrome de la sesión guardada aunque la URL no traiga todo ese contexto. Validación final en esta pasada: `get_errors` limpio en el archivo tocado y build de frontend pendiente tras aplicar el parche.
- 2026-04-08: El dashboard ahora sincroniza proyecto y secuencia activa con la URL compartible. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadieron query params `project` y `sequence` para que el estado visible quede reflejado en la barra de direcciones y pueda restaurarse al abrir ese link. La restauración desde URL tiene prioridad sobre la sesión guardada, lo que hace más fiable compartir el contexto exacto de edición para revisión o soporte. Validación final en esta pasada: `get_errors` limpio en el archivo tocado.
- 2026-04-08: Se extendió la persistencia de sesión del editor en [frontend/src/pages/dashboardLocalStorageHelpers.js](frontend/src/pages/dashboardLocalStorageHelpers.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) para recordar también el takeover del editor clásico (`activeView = editor`, `editorTakeoverMode`, `editorReturnShellSlide`). Antes, al refrescar, la sesión guardaba el proyecto y la secuencia pero degradaba siempre la vista a `library`, obligando al usuario a volver por Proyecto -> Secuencias -> Editor. Ahora, si el refresh ocurre mientras estabas dentro del editor clásico, la rehidratación vuelve directamente a esa secuencia y al mismo takeover, incluyendo playhead/bloque/timeline restaurados.
- 2026-04-08: Se restauró en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la definición de `buildTrimContext(...)` y su helper `findSequenceWordIndex(...)`, que se habían perdido durante la modularización reciente. Ese hueco rompía el arranque del trim de timeline y hacía que `Ctrl/Cmd + drag` para ajuste fino pareciera desactivado en el editor clásico, aunque la capa global de shortcuts seguía viva. Con el contexto de trim restituido, vuelven a quedar operativos el drag normal y el drag preciso sobre los handles de entrada/salida.
- 2026-04-08: Se corrigió el origen del botón Exportar cuando el usuario está dentro de un `corte en revisión` en el editor clásico. La causa raíz estaba en [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js): `buildDraftPayload(...)` serializaba el draft mediante `serializeEditorState(...)`, y ese serializador filtraba siempre las secuencias temporales `editorial-review-temp` / `split-suggestion-review-temp`. Como consecuencia, al exportar desde una revisión temporal el `activeSequenceId` dejaba de existir en el payload y el backend caía a la primera secuencia persistida del proyecto, típicamente `Bloque 1`. Ahora [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) pide explícitamente incluir esas temporales solo para export, sin cambiar el comportamiento normal de guardado/persistencia.
- 2026-04-08: Se terminó de cablear la slide 4 del library shell como superficie de exports en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), reutilizando [frontend/src/components/ProjectExportsPanel.jsx](frontend/src/components/ProjectExportsPanel.jsx) como módulo principal y reemplazando el contenido residual del viejo `editor dock` por historial de exports, preview inline para videos exportados, descarga directa, apertura del modal de estado y botón para abrir la carpeta en Explorer. También se añadió un segundo panel de acciones rápidas/resumen del último job y estilos dedicados en [frontend/src/styles/app.css](frontend/src/styles/app.css). En backend, [backend/apps/editor/services.py](backend/apps/editor/services.py) quedó alineado para guardar nuevos exports bajo la carpeta raíz del repo en `/exports/proyecto_<id>/...`, manteniendo el historial/stream/download por `ExportJob`. Validación final de esta ronda: pendiente de build/check tras aplicar el parche.
- 2026-04-08: Se corrigieron dos causas raíz en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) que estaban mezclando preview y export. Primero, durante la modularización se había perdido `layoutPreferencesRef.current = currentLayoutPreferences`; eso hacía que `saveProjectDraft(...)`, `persistEditorDraft(...)` y el payload manual de `handleExport(...)` siguieran serializando el layout por defecto, con efectos visibles como export en `landscape 16:9` y `previewSubtitlesEnabled = false` aunque el usuario estuviera viendo portrait con subtítulos activos. Segundo, el efecto que resincroniza videos auxiliares del preview (`previewBackdropVideoRef` / `splitSecondaryVideoRef`) los pausaba siempre al final, incluso si el primario seguía reproduciendo; al montar el backdrop blur también durante playback, esa pausa inmediata dejaba el fondo visualmente congelado o ausente. Ahora el ref de layout vuelve a seguir el estado visible y los auxiliares continúan reproduciendo en silencio cuando el playback está activo. Validación final: `npm run build` correcto en `frontend`.
- 2026-04-08: Se ajustó la paridad visual de subtítulos entre preview y export. En [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js), [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) los cues del preview ahora calculan un corte equilibrado de dos líneas para evitar segundas líneas huérfanas de una sola palabra corta, y el panel de preview respeta ese corte explícitamente en vez de dejar todo al wrap automático del navegador. En la misma pasada el backdrop blur del modo `fit` volvió a montarse también durante reproducción, no solo en pausa. Del lado export, [backend/apps/editor/services.py](backend/apps/editor/services.py) y [backend/apps/editor/subtitle_frame_renderer.py](backend/apps/editor/subtitle_frame_renderer.py) consumen la misma metadata de salto de línea para emitir `\N` en ASS y para renderizar overlays PNG con la misma distribución visible. Validación final: `npm run build` correcto en `frontend` y `manage.py check` correcto en `backend`.
- 2026-04-08: Se endureció también el flujo `Restaurar` de backups locales en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) para que la recuperación no dependa solo del origen actual del navegador. Se restauró `handleRestoreRetranscribeBackup(...)` y ambos restores (`before-retranscribe`, `before-append-video`) pasaron a usar un `restoreDraftBackupByReason(...)` común que, además de rehidratar el editor y recrear el `restore snapshot`, persiste inmediatamente el `editor_draft` recuperado al backend. Con esto, si luego abres el mismo proyecto por el host canónico o cambias entre sesiones, el recovery crítico ya no queda únicamente en `localStorage` del origen desde el que se restauró. Validación final: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: Se restauró la persistencia durable de secuencias recuperadas en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La regresión real no estaba solo en `Salir al selector`: el `saveProjectDraft(...)` actual ya no sincronizaba el `restore snapshot`, `persistEditorDraft(...)` había dejado de mandar `editor_draft` al backend, y `openProject(...)` se había simplificado hasta ignorar draft local, `restore snapshot` y recuperación remapeada del backup `before-retranscribe`. Resultado visible: las secuencias recuperadas reaparecían, pero el trabajo posterior sobre ellas no sobrevivía al salir y volver del editor clásico. Se restauraron `saveProjectDraft(...)`, `persistEditorDraft(...)`, `hydrateProjectDraft(...)` y el flujo robusto de `openProject(...)` para que vuelva a rehidratar desde snapshot/draft local y siga actualizando tanto almacenamiento local como backend. Validación final: `get_errors` limpio y `npm run build` correcto.
- 2026-04-08: Se restauraron dos piezas perdidas que estaban afectando directamente al editor clásico en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Primero volvió `handleReturnToLibraryShell(...)`, que era el callback detrás de `Salir al selector`; su ausencia hacía que el botón no respondiera y explotara con `ReferenceError`. Segundo volvió `loadTimelineMedia(...)`, usado por los efectos que precargan thumbnails y waveform del timeline; su ausencia generaba `ReferenceError: loadTimelineMedia is not defined` y dejaba el editor clásico parcialmente roto aunque la vista montara. Validación final: `get_errors` limpio, `npm run build` correcto y recarga del frontend sin esos runtime errors.
- 2026-04-08: Se terminó de recomponer el subgrupo restante de callbacks del crop modal en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Después de restaurar open/close todavía faltaban `handleResetSelectedCrop(...)`, `handleApplyCropModal(...)`, `handleCropPresetChange(...)`, `handleCropRectPointerDown(...)` y `handleCropResizePointerDown(...)`, que seguían siendo consumidos por [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx) a través de `PreviewCropModal`. Resultado: desapareció el `ReferenceError: handleCropPresetChange is not defined`, `get_errors` quedó limpio, `npm run build` pasó y la recarga del frontend volvió a montar sin errores de runtime.
- 2026-04-08: Se restauró el wiring del crop modal en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) después de otra pérdida parcial durante la modularización. `handleOpenCropModal(...)` había quedado apuntando a un setter inexistente (`setCropModalOpen`) y además faltaba `handleCloseCropModal(...)`, lo que tiraba el mount completo del dashboard con `ReferenceError: handleCloseCropModal is not defined`. Se corrigió el open para usar `setPreviewCropModalOpen(...)` y volvió el close que limpia también `cropModalInteractionRef`. Validación final: `get_errors` limpio, `npm run build` correcto y recarga del frontend sin ese runtime error.
- 2026-04-08: Se restauró `handleClearTranscription(...)` dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) después de otra pérdida de wiring derivada de la modularización. El botón `Borrar transcript` seguía renderizado pero el callback había desaparecido, lo que derribaba todo `DashboardPage` con `ReferenceError: handleClearTranscription is not defined` durante el mount. La función volvió con la misma responsabilidad original: borrar la transcripción vía API, limpiar draft/snapshot local, resetear selección/historial y refrescar el bootstrap. Validación final en esta pasada: `get_errors` limpio, `npm run build` correcto y recarga limpia del frontend sin ese runtime error.
- 2026-04-08: Se restauró otro bloque perdido de helpers editoriales dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) para terminar de recomponer la ruta de revisión/apertura de sugeridas después de la modularización. Volvieron `buildSequenceDraftFromEditorialAlternative(...)`, `buildEditorialCandidateReviewModel(...)`, `buildLiveSplitReviewCandidate(...)`, `buildPersistedSplitReviewItemsFromSequence(...)`, `rebuildEditorialCandidateFromWordIds(...)` e `isEditorialCandidateReviewable(...)`, que el hook [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js) sigue consumiendo como callbacks locales del page controller. Resultado de esta pasada: desapareció el `ReferenceError` que estaba cortando el flujo al abrir/revisar sugeridas y `npm run build` volvió a validar limpio desde `frontend`.
- 2026-04-08: Se añadió una redirección de origen canónico en desarrollo en [frontend/src/main.jsx](frontend/src/main.jsx) para forzar `localhost -> 127.0.0.1` antes de montar React. Motivo: varias piezas del editor siguen persistiendo en `localStorage` por proyecto/origen, incluyendo sugeridas locales, split children y otros estados del workspace; al abrir el mismo Vite server unas veces en `localhost` y otras en `127.0.0.1`, el navegador separa esos stores y daba la impresión de que las secuencias `desaparecían`. También se documentó explícitamente el uso de `127.0.0.1` en [README.md](README.md).
- 2026-04-08: Se añadió una barrera automática mínima en frontend para detectar referencias faltantes antes del runtime. En [frontend/package.json](frontend/package.json) se incorporó `npm run lint`, y [frontend/eslint.config.js](frontend/eslint.config.js) quedó configurado con `no-undef` sobre `src/**/*.{js,jsx}` y `vite.config.js`. El objetivo directo de esta ronda es cortar la secuencia de `ReferenceError` post-modularización en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) antes de llegar al navegador. También se actualizó [README.md](README.md) y la checklist para incluir este chequeo como paso operativo normal.
- 2026-04-08: Se completó el siguiente corte grande recomendado dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): el workflow editorial pesado salió hacia [frontend/src/pages/useEditorialAgentWorkflow.js](frontend/src/pages/useEditorialAgentWorkflow.js). El hook nuevo concentra reapertura y persistencia de split-review, apertura/cierre del workspace editorial, review de candidatas, aprobación/descarte, reapertura de sugeridas guardadas, restauración desde base IA, ajustes sobre sugeridas y generación/copia de consola del agente. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó como capa de wiring y derivaciones visuales para ese dominio en vez de seguir siendo dueño directo del bloque. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17079` a `10356` líneas y el hook nuevo quedó en `1409` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto ejecutado desde `frontend`. Siguiente corte recomendado según el mapa: `useProjectTranscriptionWorkflow.js`.
- 2026-04-07: Se atacó el siguiente outlier grande fuera de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js). El archivo se partió por ownership en [frontend/src/pages/dashboardCoreHelpers.js](frontend/src/pages/dashboardCoreHelpers.js), [frontend/src/pages/dashboardSequenceModelHelpers.js](frontend/src/pages/dashboardSequenceModelHelpers.js), [frontend/src/pages/dashboardSequenceTimelineHelpers.js](frontend/src/pages/dashboardSequenceTimelineHelpers.js) y [frontend/src/pages/dashboardLocalStorageHelpers.js](frontend/src/pages/dashboardLocalStorageHelpers.js), dejando a [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) como fachada estable y dueño principal de la persistencia del draft. Resultado final de la pasada: [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) bajó de `2571` a `954` líneas; los módulos nuevos quedaron en `213`, `728`, `650` y `162` líneas respectivamente. Validación final en este workspace: `get_errors` limpio en archivos tocados y `npm run build` correcto.
- 2026-04-07: Se siguió bajando el bloque editorial de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) moviendo el transporte del preview editorial a [frontend/src/pages/useEditorialAgentPreview.js](frontend/src/pages/useEditorialAgentPreview.js). Ese hook ahora posee el playback state, las refs de video del preview editorial, la navegación por bloques y la continuidad multipart, mientras la página conserva solo el wiring y las derivaciones visuales. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17229` a `17079` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto ejecutado desde `frontend`. Nota de criterio añadida al mapa: el objetivo de `~1000` líneas queda fijado como techo blando por archivo; el promedio actual de `frontend/src/**/*.js,jsx` ya está en `709.83`, así que el foco real pasa a ser reducir outliers grandes.
- 2026-04-07: Se siguió la fase editorial de modularización sacando el subflujo del modal de ampliación de rango fuera de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). El nuevo hook [frontend/src/pages/useEditorialRangeSelection.js](frontend/src/pages/useEditorialRangeSelection.js) concentra el estado local del modal, el modelo derivado de párrafos/candidatas solapadas, la selección por drag y los resets/sincronizaciones del rango. [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) conserva por ahora solo la expansión final contra backend y la orquestación editorial más amplia. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17440` a `17229` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto.
- 2026-04-07: Se añadió un mapa operativo de división para [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), con catálogo de áreas, funciones representativas, estado de avance y archivo destino por subdominio. El detalle quedó en [docs/DASHBOARD_PAGE_DIVISION_MAP.md](docs/DASHBOARD_PAGE_DIVISION_MAP.md), y arriba de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó una nota corta de referencia para mantener visible el orden de extracción sin volver a improvisar cortes. Esta pasada no cambió comportamiento: ordena el refactor y fija la meta de salida del page controller.
- 2026-04-07: Empezó la siguiente fase profunda de modularización de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) sacando un subdominio con estado propio en vez de otro helper suelto. La sesión local del modal de transcripción, junto con sus mutadores `patch/append/upsert` para entradas de consola y control de proceso, salió a [frontend/src/pages/useTranscriptionModalSession.js](frontend/src/pages/useTranscriptionModalSession.js). [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) sigue orquestando el flujo, pero dejó de ser dueño directo de ese bucket de estado interno. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17483` a `17440` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: la meta razonable ya es dejar a `DashboardPage` como coordinador de pantallas y flujos cruzados; las siguientes 1-2 fases útiles deberían extraer otros subdominios con estado propio, no seguir persiguiendo helpers pequeños.
- 2026-04-07: Se cerró el último helper local que seguía viviendo arriba de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) con JSX propio. La mini timeline editorial salió a [frontend/src/components/EditorialAgentMiniTimeline.jsx](frontend/src/components/EditorialAgentMiniTimeline.jsx), y [frontend/src/components/EditorialAgentModal.jsx](frontend/src/components/EditorialAgentModal.jsx) pasó a consumirla directamente en vez de depender de un callback/render helper inyectado desde la página. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17531` a `17483` líneas y el archivo ya arranca directamente en `function DashboardPage()` después de sus constantes. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: ya no quedan cortes obvios de helpers locales; lo siguiente, si se sigue, es una modularización más profunda de hooks/sections internas del componente, no otra ronda simple de extraer helpers sueltos.
- 2026-04-07: Se remató la limpieza del tope de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) moviendo los dos helpers no visuales que aún quedaban inline (`bootstrapHasTranscriptMaterialized` y `parseJsonTextSafely`) hacia [frontend/src/pages/dashboardStatusHelpers.js](frontend/src/pages/dashboardStatusHelpers.js). Con eso, arriba del page controller ya solo queda `renderEditorialAgentMiniTimeline(...)` como helper local con JSX, y el resto del bloque superior pasó a depender de módulos de dominio. Resultado acumulado de esta pasada final: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `17547` a `17531` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: el siguiente paso ya no es otro helper-pack grande, sino decidir si conviene dejar la mini timeline local o moverla a un subcomponente/editorial helper con JSX.
- 2026-04-07: Se completó otra pasada fuerte de modularización sobre [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) moviendo casi todo el bloque editorial puro que todavía seguía arriba del page controller hacia [frontend/src/pages/dashboardEditorialHelpers.js](frontend/src/pages/dashboardEditorialHelpers.js). Ese módulo ya concentra la normalización del estado del agente editorial, snapshots, consola/etapas, metadata de títulos, compatibilidad de sugeridas, helpers de overlap/shared ids, resolución de visual guide, aspect display plans y payloads de clips/editorial gaps. Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `18577` a `17547` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: arriba del page controller ya solo quedan `bootstrapHasTranscriptMaterialized`, `parseJsonTextSafely` y la mini timeline JSX editorial; el siguiente corte ya no es otro bloque grande, sino una limpieza final pequeña de helpers residuales/UI local.
- 2026-04-07: Se completó otra pasada rentable de modularización sobre [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) moviendo dos clusters puros fuera del page controller. Primero, la utilería editorial/suggested/OpenAI que seguía arriba del archivo salió a [frontend/src/pages/dashboardEditorialHelpers.js](frontend/src/pages/dashboardEditorialHelpers.js), centralizando helpers como `buildManualSplitFromCandidate`, `buildStoredEditorialAgentPayload`, `mergeSuggestedAlternatives`, `resolveTimelineMediaUrl`, `buildOpenAIRunUsageSummary` y el resto de resolución contextual/editorial. Segundo, la utilería de preview/crop/aspect y persistencia del offset del transcript pasó a [frontend/src/pages/dashboardPreviewHelpers.js](frontend/src/pages/dashboardPreviewHelpers.js), que ahora concentra `buildPreviewSubtitleCues`, normalización de crop rect, ajustes por aspecto, estilos de cover/fit y la persistencia local del offset temporal por proyecto. Resultado acumulado de esta ronda: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `19143` a `18577` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: ya no queda mucho JSX repetido grande; lo que resta son 2-3 cortes de helpers/editorial state más una pasada final de limpieza.
- 2026-04-07: Se completó la siguiente fase de reducción de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en dos frentes. Primero, la UI inline del menú de subtítulos del preview salió a [frontend/src/components/PreviewSubtitleMenu.jsx](frontend/src/components/PreviewSubtitleMenu.jsx), y [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx) dejó de depender de un render callback local de la página para ese bloque. Segundo, arrancó la extracción de helpers puros hacia [frontend/src/pages/dashboardStatusHelpers.js](frontend/src/pages/dashboardStatusHelpers.js), que ahora concentra la primera tanda de utilería de estado/export/transcripción (`createTranscriptionModalEntry`, `formatTranscriptionStatusReportEntry`, `buildSuggestedExportName`, `getExportStageProgress`, `downloadExportFile`, `buildOpenAITraceConsoleEntries`, `formatTranscriptionDuration`, `formatTranscriptionClock`). Resultado acumulado de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `19378` a `19143` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado actual de lo que falta: la extracción del menú UI ya quedó cerrada; la fase restante pasa a ser seguir moviendo más clusters puros fuera del page controller.
- 2026-04-07: Se siguió la modularización de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) sacando otro bloque grande de UI inline: el modal de depuración quedó movido a [frontend/src/components/DepurateModal.jsx](frontend/src/components/DepurateModal.jsx), junto con su `EMPTY_DEPURATE_TRACE`, de modo que el page controller conserva ya solo el estado y los handlers de depuración. Resultado inmediato de esta pasada: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó de `19763` a `19378` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto. Estado de la ronda de modularización a esta fecha: ya quedaron fuera el preview clásico compartido, los overlays principales y `DepurateModal`; lo que sigue como siguiente corte visible son el menú UI de subtítulos del preview y luego varios clusters de helpers puros todavía mezclados dentro del page controller.
- 2026-04-07: Se completó otra ronda de modularización visual en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) extrayendo dos zonas densas que todavía seguían inline. Primero, el preview repetido del editor clásico salió hacia [frontend/src/components/ClassicEditorPreviewPanel.jsx](frontend/src/components/ClassicEditorPreviewPanel.jsx), de modo que el dock/editor reutilizan ahora el mismo panel y el mismo playerbar en vez de duplicar JSX e iconografía. Después, los overlays de estado y detalle que seguían incrustados en el page controller pasaron a [frontend/src/components/DashboardOverlayModals.jsx](frontend/src/components/DashboardOverlayModals.jsx): modal de export, modal de transcripción, detalle de sugerida y crop modal. En la misma pasada se corrigió una rotura de JSX dejada por el corte inicial del crop modal y el preview fuente quedó otra vez estable dentro del script workspace. Resultado: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó a `19763` líneas y quedó todavía más cerca de un rol de coordinación. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto.
- 2026-04-07: Se completó otra ronda de modularización de UI pesada en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) extrayendo el modal editorial grande y su modal de ampliación de rango hacia [frontend/src/components/EditorialAgentModal.jsx](frontend/src/components/EditorialAgentModal.jsx). El page controller dejó de cargar inline toda la revisión editorial, previews duales, lista de candidatos y selección de párrafos para expandir rango; ahora solo orquesta estado, callbacks y helpers agrupados. En paralelo, [frontend/src/components/suggestedSequenceCardHelpers.js](frontend/src/components/suggestedSequenceCardHelpers.js) exporta también `resolveEditorialWhyText(...)` para que el nuevo componente reutilice la misma lógica editorial compartida en vez de duplicarla. Resultado: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) bajó a `20603` líneas y quedó más cerca de una capa de coordinación. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto.
- 2026-04-07: Se completó otra ronda de modularización estructural de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) enfocada ya no en helpers de storage sino en bloques UI completos. El `SplitReviewModal` salió del page controller hacia [frontend/src/pages/SplitReviewModal.jsx](frontend/src/pages/SplitReviewModal.jsx), manteniendo su lógica de revisión, timeline editable, video multipart y persistencia local fuera del archivo gigante. En paralelo, el banner superior del workspace editorial dejó de renderizarse inline y pasó a [frontend/src/components/EditorialWorkspaceBanner.jsx](frontend/src/components/EditorialWorkspaceBanner.jsx), reutilizando además el helper compartido de colores en [frontend/src/components/editorialSplitCandidateColors.js](frontend/src/components/editorialSplitCandidateColors.js). Resultado: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) quedó más cerca de un rol de orquestación, con menos JSX denso embebido y menos componentes locales definidos dentro del mismo archivo. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto.
- 2026-04-07: Se avanzó otra ronda de modularización enfocada en el chrome de `Secuencias sugeridas` dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). La composición repetida de tarjetas se unificó alrededor de un view-model compartido en [frontend/src/components/suggestedSequenceCardHelpers.js](frontend/src/components/suggestedSequenceCardHelpers.js) (`buildSuggestedSequenceCardViewModel(...)`), eliminando en ambos renderers la duplicación de textos, duración, labels, `format fit` y metadatos base de `SuggestedSequenceCard`. En paralelo, [frontend/src/pages/suggestedSequenceStorage.js](frontend/src/pages/suggestedSequenceStorage.js) absorbió también la persistencia de colapso/desaprobación de sugeridas y el helper de toggle de ids, de modo que [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de cargar otra capa de `localStorage` y `Set` mutations inline para esta zona. Resultado de la pasada: el segundo renderer de tarjetas quedó alineado con el primero, se redujo otra porción de lógica duplicada del page controller y el archivo principal bajó de `21117` a `21004` líneas. Validación final en este workspace: `get_errors` limpio en los archivos tocados y `npm run build` correcto.
- 2026-04-09: Se siguió la ronda metódica de reducción de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) con tres helpers puros más, todos fuera del page controller: [frontend/src/pages/previewAudioStorage.js](frontend/src/pages/previewAudioStorage.js) para volumen/mute persistidos del preview, [frontend/src/pages/splitModalConfigStorage.js](frontend/src/pages/splitModalConfigStorage.js) para la configuración guardada del `SplitReviewModal`, y [frontend/src/pages/splitReviewProgressStorage.js](frontend/src/pages/splitReviewProgressStorage.js) para el progreso local de revisiones parciales. Con estas extracciones adicionales el archivo principal bajó de `21199` a `21117` líneas, manteniendo el mismo comportamiento y validación limpia (`get_errors` sin problemas y `npm run build` correcto). Sigue siendo demasiado grande, pero ahora tiene más responsabilidades movidas a módulos de dominio y menos ruido de `localStorage` incrustado dentro del controlador principal.
- 2026-04-09: Se redujo todavía más la agresividad del fallback de reanudación del preview, porque el síntoma `arranca y se reinicia` seguía siendo compatible con un segundo seek disparado por el propio helper aunque el clip nuevo ya estuviera entrando. En [frontend/src/pages/sequencePreviewVideoHelpers.js](frontend/src/pages/sequencePreviewVideoHelpers.js) `waitForPresentedVideoFrame(...)` ahora devuelve si hubo avance real de frame, y `resumeVideoPlayback(...)` dejó de interpretar una sola ventana corta sin avance como motivo suficiente para re-seekear. El helper espera una segunda ventana de observación y solo aplica el `nudge` correctivo si el video terminó realmente `paused`; si el decoder simplemente tarda unas décimas en presentar el frame nuevo, ya no fuerza un reinicio del clip. Validación final en este workspace: chequeo de errores limpio y `npm run build` correcto.
- 2026-04-09: Se corrigió una causa raíz adicional del síntoma `doble play` en transiciones clip→clip del editor clásico. En [frontend/src/pages/sequencePreviewVideoHelpers.js](frontend/src/pages/sequencePreviewVideoHelpers.js) `waitForPresentedVideoFrame(...)` estaba tratando el primer `requestVideoFrameCallback` como avance válido aunque el `mediaTime` siguiera todavía en el mismo instante tras el seek. Eso hacía que `resumeVideoPlayback(...)` clasificara falsamente la reproducción como `stalled` y disparara un segundo fallback de seek/play justo al entrar al clip nuevo. Ahora la espera de frame sigue observando callbacks hasta que el tiempo realmente avance o venza el timeout, evitando reanudar dos veces sobre la misma transición. Validación final en este workspace: chequeo de errores limpio y `npm run build` correcto.
- 2026-04-07: Se corrigió otra causa raíz del síntoma `doble play` al pasar de un clip a otro en la timeline del editor clásico. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) varias rutas críticas hacían `seekMultipartVideo(...)` y acto seguido llamaban a `playMultipartVideo(...)`, pero ese helper volvía a hacer otro `seekMultipartVideo(...)` por dentro antes de reanudar. En la práctica, la transición clip→clip podía terminar haciendo doble seek sobre el mismo inicio de bloque, lo que encaja con el efecto audible/visual de `arranca dos veces` justo al cambiar de clip. Ahora, en esas rutas que ya llegan con seek resuelto, el flujo pasa a `resumeVideoPlayback(...)` directamente sobre el tiempo local ya alineado, dejando un solo seek real antes de reanudar. Validación final en este workspace: chequeo de errores limpio y `npm run build` correcto.
- 2026-04-07: Se corrigió una causa probable del síntoma `el clip parece arrancar dos veces` en el editor clásico y se siguió segmentando el transporte. En [frontend/src/pages/sequencePreviewVideoHelpers.js](frontend/src/pages/sequencePreviewVideoHelpers.js) se extrajeron `ensureVideoMetadata`, `seekVideo`, `waitForPresentedVideoFrame` y `resumeVideoPlayback` fuera de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), dejando la lógica de seek/reanudación en un módulo propio en vez de incrustada dentro del page controller. En esa misma extracción, `resumeVideoPlayback(...)` dejó de forzar un segundo `video.play()` ciego tras el seek: ahora solo reintenta si el video quedó realmente pausado/estancado y, si necesita nudging, reutiliza el mismo playback en vez de re-disparar el arranque desde cero. Esto apunta directamente al síntoma de doble inicio audible/visual al apretar espacio o al cruzar de clip durante reproducción continua. Validación final en este workspace: chequeo de errores limpio y `npm run build` correcto.
- 2026-04-07: Se avanzó otra ronda de modularización enfocada en las sugeridas del editor clásico y en sacar peso directo de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx). Se extrajeron [frontend/src/components/SuggestionThumbnailStrip.jsx](frontend/src/components/SuggestionThumbnailStrip.jsx) para la filmstrip hover/cacheada y [frontend/src/components/suggestedSequenceCardHelpers.js](frontend/src/components/suggestedSequenceCardHelpers.js) para la composición visual/textual de tarjetas sugeridas (format fit, strength, precision, copy, pace, dedupe). En [frontend/src/components/SuggestedSequenceCard.jsx](frontend/src/components/SuggestedSequenceCard.jsx) el componente quedó generalizado para soportar colapso, click configurable de tarjeta y contenido extra reutilizable. Con eso, la segunda lista inline de sugeridas dentro de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) dejó de renderizar un `<article>` duplicado y pasó a reutilizar el mismo componente compartido que la primera lista. Validación final en este workspace: chequeo de errores limpio en los 4 archivos tocados; no se ejecutó build completo en esta ronda.
- 2026-04-09: Se remató la ergonomía del transcript de secuencia para que el punto activo quede realmente centrado en el módulo vertical y no se "pegue" al borde inferior al acercarse al final del texto. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadió `sequenceTranscriptCenterOffset` calculado desde la altura real del contenedor con `ResizeObserver`, se replicó en ambos renders del transcript el patrón de `top/bottom spacer` ya usado por el transcript general, y se unificaron los focos de `playhead`, click sobre palabra y click/scrub desde timeline a `targetViewportRatio: 0.5`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadió `.workflow-sequence-transcript-spacer` como separador no interactivo. Resultado: el cursor activo puede mantenerse visualmente en el centro aun en las últimas líneas, con más contexto arriba y abajo. Validación final en este workspace: `npm run build` correcto.
- 2026-04-06: Se reforzó el flujo `Depurar secuencia` para volverlo auditable y más explícito en contexto. En [backend/apps/editor/services.py](backend/apps/editor/services.py) la depuración ahora arma y persiste un `analysis_context` con contexto general del proyecto, contexto editorial de la secuencia y mapa de párrafos (`sequence_paragraph_map`) antes de llamar a OpenAI; el prompt se amplió para exigir análisis párrafo por párrafo, decisión `keep/bridge/trim` y retención de hechos útiles en `context_retention`. En esa misma función se corrigió además un hueco previo: el resultado final ahora devuelve `content_analysis` y `_openai_meta`, que antes quedaban fuera del payload consumido por el modal. En [backend/apps/editor/views.py](backend/apps/editor/views.py) cada corrida asíncrona de depuración guarda y expone su propia traza persistida (`trace`) filtrada por `operation_name="sequence-depuration"`, incluyendo request/response guardados, provider request id, tokens y costo estimado. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el `DepurateModal` se amplió con: explicación previa de cómo trabaja el análisis, terminal API dentro del modal con payloads request/response guardados, métricas `in/out/total/costo`, mapa de contexto usado, decisiones por párrafo y memoria de contexto retenido. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el modal ganó layout más ancho y estilos densos para soportar esa auditoría. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-09: Se implementaron 4 mejoras en la gestión de secuencias editoriales. (1) **Aprobar división → secuencias permanentes**: en `DashboardPage.jsx` `handleSplitReviewApproved` ahora, tras actualizar el bootstrap, llama a `pushUndoSnapshot()`, mapea `taggedSubSequences` con `buildSequenceDraftFromEditorialAlternative` y añade los drafts resultantes a `sequenceDrafts` + `requestPersist()` — ya no solo guarda en localStorage/bootstrap temporal. El mensaje de estado cambió a "N secuencias creadas." (2) **approvedCandidateIdSet**: se añadió un `useMemo` (cerca del estado `dividedCandidateIds`) que computa un `Set` de los `alternativeId` de todas las secuencias no transitorias con `editorialSource.alternativeId`, para detectar qué candidatas ya están guardadas como secuencias. (3) **Etiquetas de "Secuencias creadas"**: tanto en el módulo de biblioteca (`library-sequences-created`) como en el panel workflow shell, el label pasó de `"Secuencia N"` + span separado a `"N. [nombre acortado]"` en la etiqueta `<strong>`. (4) **Grupo "Secuencias aprobadas" en Secuencias sugeridas**: el módulo `library-sequences-suggested` ahora separa los candidatos en `approvedCandidates` (ya en `sequenceDrafts`) y `pendingCandidates`, renderizados con cabeceras de grupo; las aprobadas muestran badge "Aprobada" y botón "Abrir en editor" (llama a `handleOpenSequenceInClassicEditor` con el id del draft correspondiente), y reciben clase CSS `is-approved`. Validación: `npx vite build --mode development` correcto.
- 2026-04-06: Se corrigió una race condition que hacía que el `SplitReviewModal` (y los demás modos del agente editorial) mostrara "0 clips" al terminar el análisis. El error estaba en los cuatro jobs de backend (`_run_editorial_agent_job`, `_run_editorial_refine_job`, `_run_editorial_adjust_job`, `_run_editorial_split_job`) en [backend/apps/editor/views.py](backend/apps/editor/views.py): el `progress_callback` con `percent=100` llamaba a `_update_editorial_agent_run(status="ready")` SIN incluir las `alternatives`; si el cliente del frontend hacía un poll en esa ventana (antes de que el job llamara a `_set_editorial_agent_run` con los datos completos), recibía `status:"ready"` + `alternatives:[]`, lo que hacía `setSegments([])` y `setPhase("review")` → modal en revisión con 0 clips y sin timeline. **Fix backend**: se cambió el progress_callback de los cuatro jobs para que siempre emita `status="processing"` (progress capped a 99), delegando el único `status="ready"` al `_set_editorial_agent_run` final que ya tiene las `alternatives`. **Fix frontend defensivo**: en `DashboardPage.jsx`, `handleAnalyze` ahora salta el ciclo (`continue`) si `status==="ready"` pero `alts.length===0`, esperando el siguiente poll en lugar de entrar en review vacío. Validación: `npx vite build --mode development` correcto.
- 2026-04-08: Se corrigió la causa raíz por la que aplicar depuración (y en general la primera edición tras abrir un proyecto) no creaba un punto de undo. El `useEffect([activeProjectId])` definido en la línea ~16917 de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) llamaba a `resetHistoryState()` incondicionalmente, incluyendo cuando `activeProjectId` cambiaba de `null` → proyecto (apertura). Como ese efecto corre DESPUÉS del `commitHistoryIfNeeded` (definido antes, línea ~15241), el `historyRef` que `initializeHistoryState` había poblado sincrónicamente dentro de la función async de carga quedaba borrado. El primer `commitHistoryIfNeeded` post-edición añadía el estado post-cambio como única entrada a `index 0`, con lo que `undoDepth = max(0,0) = 0` y el botón Undo nunca se habilitaba tras la primera operación. **Fixes**: (1) en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), el efecto ahora solo llama a `resetHistoryState()` cuando `!activeProjectId` (proyecto cerrado/nulo); (2) en [frontend/src/pages/useDashboardHistory.js](frontend/src/pages/useDashboardHistory.js), `initializeHistoryState` ahora limpia `pendingHistoryCommitRef` y `suppressHistoryCommitRef` para evitar que flags colgantes de la sesión anterior del proyecto se transfieran al nuevo proyecto. Con esto el histórico queda correctamente inicializado en la carga del proyecto y el botón Undo se habilita tras la primera edición (depuración, corte, desactivación de palabras, etc.)
- 2026-04-07: Se mejoraron cuatro aspectos del `SplitReviewModal` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [backend/apps/editor/services.py](backend/apps/editor/services.py). (1) **Instrucciones fuzzy**: en `services.py` el encabezado del bloque `_custom_instructions_block` ahora advierte a la IA que las correcciones del editor son aproximadas — puede haber errores tipográficos o variantes fonéticas — y pone ejemplos concretos (`Gilly→Geely`, `Hondai→Hyundai`) para que la IA reconozca y corrija variantes aunque el editor no escribió exactamente el nombre correcto. (2) **Fix barra espaciadora con foco en botones**: se eliminó `"button"` del array de tags excluidos en el handler `keydown` del modal (solo quedan `"input"` y `"textarea"`), y los botones ‹ y › ahora llaman a `e.currentTarget.blur()` tras el click para devolver el foco al documento, permitiendo que la barra espaciadora funcione como play/pause en todo momento durante la revisión. (3) **Timecode + control de volumen**: se añadieron estado `volume` (default 1) y un efecto `useEffect` que sincroniza ese valor con `videoRef.current.volume` al cambiar o al cambiar de segmento. La barra de controles ahora incluye un timecode compacto `M:SS / M:SS` mostrando posición del playhead dentro del candidato y duración total, más un slider de volumen a la derecha. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron `.split-review-timecode`, `.split-review-volume-row`, `.split-review-volume-icon` y `.split-review-volume-slider` con el look denso habitual (sin border-radius en el track, thumb pequeño azul). (4) **Persistencia de configuración del modal**: se añadieron helpers `loadSplitModalConfig(projectId)` / `saveSplitModalConfig(projectId, config)` (localStorage key `video-editor:split-modal-config:v1`). Los estados `interviewMode`, `targetDurations`, `presetRules` y `customInstructions` se inicializan con `lazy initializer` leyendo la config guardada para ese proyecto. `handleAnalyze` guarda la config antes de enviar al backend, así el modal recuerda los ajustes entre sesiones. Las sequence_names y contexto editorial (editorial_rationale, conversation_summary) ya existían en el payload aprobado vía `...seg` spread — se confirmó que están correctamente preservados al aprobar la división. Validación: `npx vite build --mode development` correcto.
- 2026-04-06: Se añadió sub-división recursiva de clips en la fase de revisión del `SplitReviewModal`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) cada bloque del timeline ahora muestra un botón `⇄` al hacer hover (o cuando está activo). Al clickearlo llama a `handleSubdivide(e, segIdx)`: construye un sub-candidato con los ms y word_ids del segmento actual (ajustados por las fronteras arrastradas), llama a la misma API `/split-suggestion/` con la misma configuración (interview_mode, duraciones, reglas, instrucciones), muestra un spinner/mensaje de progreso como overlay dentro del bloque mientras analiza, y al recibir los sub-segmentos los inserta en el array `segments` (splice en posición `segIdx`) y añade `N-1` nuevas fronteras internas al array `boundaries` (splice en `segIdx` sin eliminar ninguna). Si el sub-análisis devuelve un solo segmento (no se puede dividir más), simplemente descarta el resultado. El estado `subdivideState` bloquea todos los demás controles mientras hay un análisis secundario en curso. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron `.split-review-tl-subdivide-btn`, `.split-review-tl-block-analyzing`, `.split-review-tl-block-spinner` y `.split-review-tl-block-analyzing-msg`. Validación: `npm run build` correcto.
- 2026-04-06: Se reforzaron las reglas del prompt de división de IA y se actualizó la UI de configuración. En [backend/apps/editor/services.py](backend/apps/editor/services.py): (1) `_FORMAT_RULES` se inyecta siempre en el `_system_prompt` de ambos modos (entrevista y genérico) — reglas permanentes de formato para precios en quetzales (`Q.XXX,XXX.XX`), potencia (`hp`), velocidad (`km/h`), torque (`Nm`), peso (`kg`) y distancias. (2) `_SEMANTIC_PHILOSOPHY` también hardcodeada: la IA debe mapear todos los temas antes de proponer cortes, identificar cuándo un tema inicia y termina, y reconocer si preguntas consecutivas son complemento de una misma idea. (3) `_hard_max_seconds = max(target_duration_list) * 2` añadido como `LÍMITE DURO` en `_rules`: ningún clip puede superar ese valor; si lo haría, la IA debe buscar la transición semántica más natural dentro de esa ventana. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la sección de instrucciones de la fase config fue reemplazada por dos checkboxes (`Precios en quetzales`, `Unidades técnicas`) con estado `presetRules` por defecto activos (aunque las reglas de formato ya son siempre activas en backend) más un textarea reducido (2 filas) solo para correcciones de nombres/marcas. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron estilos para `.split-review-rules-list`, `.split-review-rule-row` y `.split-review-rule-desc`. Validación: `npm run build` correcto.
- 2026-04-06: Se mejoró la fase de revisión del `SplitReviewModal` con cuatro cambios. (1) **Fix video multipart**: el seek al cambiar de segmento ahora usa `resolveMultipartLocalSeconds(start_ms, currentPart)` en vez de dividir directamente el ms global; lo mismo para `endSec` en el handler `timeupdate`, eliminando el síntoma de "reproduce la posición equivocada en proyectos con video compuesto". (2) **Playhead arrastrable en el timeline**: se añadió estado `playheadMs` que se actualiza en cada `timeupdate` del video; el playhead aparece como flecha blanca + línea vertical en la barra de timeline y puede arrastrarse para hacer seek en tiempo real; al cruzar una frontera de segmento también cambia `currentSegmentIdx` automáticamente. (3) **Layout**: el video preview está ahora encima y el timeline de bloques/handles/playhead está debajo del preview, con hint de espacio/teclado eliminado visualmente. (4) **Campo de instrucciones de corrección**: en la fase de configuración se añadió un `<textarea>` con placeholder explicativo; su contenido se envía como `custom_instructions` en el `splitConfig`. En [backend/apps/editor/services.py](backend/apps/editor/services.py) se parsea y construye `_custom_instructions_block` que se concatena al `_system_prompt` de ambos modos (entrevista y genérico) con encabezado `INSTRUCCIONES ADICIONALES DEL EDITOR`. Validación: `npm run build` correcto.
- 2026-04-06: Se rediseñó la fase de revisión del modal `SplitReviewModal`. La fila de pestañas/tarjetas fue reemplazada por una **barra de timeline horizontal** con bloques de color proporcionales a la duración de cada segmento y **handles arrastrables** entre ellos. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) se añadió estado `boundaries` (array de ms entre segmentos), refs `timelineRef` / `dragStateRef`, memos `allSequenceWords` y `wordSegmentIndex`, y funciones `handleTimelineDragStart/Move/End` con pointer capture. Al arrastrar un handle la frontera se desplaza y `wordSegmentIndex` (que mapea word.id → segmento según qué lado de la frontera tiene `word.start_ms`) se recalcula en tiempo real. El transcript ahora muestra **todas las palabras de la secuencia** con fondo de color del segmento al que pertenecen en ese momento; las palabras del segmento activo aparecen a plena opacidad y el resto atenuado. Al aprobar, `handleApprove` reconstituye los segmentos con sus `start_ms`/`end_ms`/`word_ids` ajustados por las fronteras actuales antes de llamar a `onApproved`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron clases para `.split-review-timeline`, `.split-review-tl-block`, `.split-review-tl-handle` y `.split-review-transcript--shaded`. Validación: `npm run build` correcto.
- 2026-04-06: Se reescribieron los prompts de IA de `generate_ai_split_suggestions` en [backend/apps/editor/services.py](backend/apps/editor/services.py) para reflejar la filosofía correcta del feature: el contexto conversacional completo es obligatorio, la duración es solo una referencia orientativa. Tanto `interview_mode` como el modo genérico ahora incluyen `REGLA PRINCIPAL` explícita y reglas `NUNCA cortes en mitad de una respuesta/argumento`, con `_duration_label` marcado como `(no vinculante)`. El objetivo es que la IA busque fronteras naturales de conversación, no que fuerce cortes en tiempos arbitrarios; los clips resultantes son material en bruto para edición posterior en timeline.
- 2026-04-06: Los botones de duración del modal `SplitReviewModal` pasaron de selección exclusiva a multi-selección. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `targetDuration` (int) fue reemplazado por `targetDurations` (Set, default `{40}`), con lógica de toggle que impide deseleccionar el último valor. El payload enviado al backend ya incluye `target_duration_seconds` como array ordenado. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `_split_cfg` acepta tanto int como lista, construye `_duration_label` con todos los valores y los incorpora en los prompts. Validación: `npm run build` correcto.
- 2026-04-06: Se corrigieron dos problemas del modal `SplitReviewModal` en la vista biblioteca: no aparecía y el cuerpo se colapsaba. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el modal solo estaba renderizado en el return branch del editor (`activeView !== "library"`); se añadió también al branch de biblioteca junto a los demás modales de estado. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadió `min-height: 340px` a `.split-review-modal` y se corrigió `.split-review-modal-body` de `flex: 1 1 0` a `flex: 1 1 auto` para que el contenido no se colapse a cero altura dentro de un contenedor sin altura explícita. Validación: `npm run build` correcto.
- 2026-04-05: Se anuló temporalmente el slide 4 del library-shell para que el wheel/scroll no siga empujando al `editor dock` cuando la ruta principal de edición fina sigue siendo el editor clásico. En [frontend/src/pages/dashboardShellHelpers.js](frontend/src/pages/dashboardShellHelpers.js) `LIBRARY_PROJECT_SLIDE_ORDER` volvió a cerrar en `Slide 3` y quedó explícito `LIBRARY_EDITOR_DOCK_ENABLED = false`; en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ese flag también deja de montar la cuarta sección del shell y desactiva su panel menu, evitando el `doble scroll` hacia una superficie secundaria que por ahora no debe intervenir. Validación final en este workspace: chequeo de errores del editor en los archivos tocados, sin errores.
- 2026-04-05: Después de comparar un frame real de export portrait con el look del preview, se cerró también la brecha de `presencia visual` del subtítulo, no solo su geometría. En [frontend/src/config/subtitleStyleSpec.json](frontend/src/config/subtitleStyleSpec.json) `Accent Pop` ahora define `baseBlur`, outline y shadow más suaves para export; y en [backend/apps/editor/subtitle_style_registry.py](backend/apps/editor/subtitle_style_registry.py) + [backend/apps/editor/services.py](backend/apps/editor/services.py) el `.ass` aplica blur base persistente en toda la línea y reduce el borde duro del estilo base. El objetivo fue acercar el export al shadow-stack del preview CSS y alejarlo del look `caption con contorno rígido` que todavía se veía en el frame exportado.
- 2026-04-05: Se afinó la paridad visual `preview -> export` para que los formatos de salida más comunes del producto (`1080x1920`, `1920x1080`, `1080x1080`) compartan la misma geometría relativa del subtítulo. En [frontend/src/config/subtitleStyleSpec.json](frontend/src/config/subtitleStyleSpec.json) se añadieron perfiles por aspecto (`portrait`, `square`, `landscape`) y el límite fijo de `720 px` dejó de ser la referencia global del export. En [frontend/src/subtitleStyleRegistry.js](frontend/src/subtitleStyleRegistry.js), [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) el preview pasó a escalar el bloque con unidades del contenedor (`cqw`/`cqh`) y margen inferior proporcional al alto del stage, para que el mockup visual use la misma regla geométrica que el render final. En [backend/apps/editor/subtitle_style_registry.py](backend/apps/editor/subtitle_style_registry.py) el export `.ass` ahora resuelve el ancho útil por aspecto en vez de cap fijo, con valores efectivos aprox. `78%` en portrait, `76%` en square y `54%` en landscape. Validación final en este workspace: `npm run build`, `manage.py check` y verificación programática de medidas para `1080x1920`, `1920x1080` y `1080x1080` correctas.
- 2026-04-05: Se reemplazó la duplicación ad hoc de estilos de subtítulos por una especificación compartida entre preview y export. Se añadió un registro común en [frontend/src/config/subtitleStyleSpec.json](frontend/src/config/subtitleStyleSpec.json), un adaptador web en [frontend/src/subtitleStyleRegistry.js](frontend/src/subtitleStyleRegistry.js) y un adaptador backend en [backend/apps/editor/subtitle_style_registry.py](backend/apps/editor/subtitle_style_registry.py), de modo que opciones, defaults, familias, tamaños y tokens visuales de `Accent Pop` ya no queden repartidos en CSS/React/Python con riesgo de drift. En la misma ronda el export `.ass` pasó a leer la misma familia/tamaño/layout base, eliminó el `ScaleX/ScaleY` inflado, recuperó un ancho útil equivalente al preview (`90%` con tope `720 px`) y dejó preparada la arquitectura para no habilitar estilos futuros hasta que ambos renderers los soporten. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-05: Se recalibró la traducción de subtítulos `preview -> export` para que tamaño y posición dejen de verse corridos entre el player web y el MP4 final. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `_resolve_export_ass_style_settings(...)` estaba usando una caja útil demasiado angosta (`460 px` en portrait), un margen inferior algo más bajo y una equivalencia conservadora de tamaño (`75 px`) respecto al preview CSS, que en realidad arma el bloque con `bottom: 20px`, `max-width: min(90%, 720px)` y escala activa `1.22`. Ahora el export en portrait sale con `81 px`, `720 px` de ancho útil, `65 px` de margen vertical, escala base `104%` y highlight `122%`, acercando el bloque al tamaño/posición del preview. Validación final en este workspace: export directo del proyecto 25 completado como `ExportJob 23`, con frame de control en `backend/media/exports/project_25/export_23_frame_16s.png`.
- 2026-04-05: Se corrigió la causa raíz del export que se quedaba clavado cerca del segundo clip, consumía memoria y daba la impresión de `ya no usa CUDA`. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el modo `fit` componía cada panel sobre un `color=black` infinito y las dos `overlay` dejaban repetir ese frame base al terminar el video real; al entrar un bloque `fit`, el `concat` no recibía un `EOF` limpio, el render se detenía alrededor del cambio de clip y FFmpeg acababa con `10000 buffers queued` y `Cannot allocate memory`. Ahora ambas overlays de `_build_fit_panel_filter(...)` fuerzan `shortest=1:eof_action=endall`, así que cada clip `fit` termina exactamente con su fuente real y el pipeline puede seguir concatenando sin inflar buffers. Validación final en este workspace: export directo del proyecto 25 completado como `ExportJob 21`, con `video_codec = h264_nvenc`, log en `backend/media/exports/project_25/export_21.log` y MP4 válido de `22.15s` verificado con `ffprobe`.
- 2026-04-05: Se simplificó otra vez el botón `Download` del modal de export para corregir el caso `aprieto y no pasa nada`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la descarga dejó de pasar por `fetch -> blob -> showSaveFilePicker`, porque ese flujo hacía trabajo asíncrono antes del guardado y podía perder la activación del gesto del usuario, dejando el botón aparentemente muerto o dependiendo de un `file picker` inestable. Ahora el botón dispara una descarga directa del endpoint `/api/editor/exports/<id>/download/` mediante un anchor temporal y deja que el navegador gestione el attachment con el filename que ya entrega el backend. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió una causa raíz de `el transcript muestra un bloque pero abajo no aparece su clip`. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) `deriveSequenceBlocks(...)` estaba heredando el `customSplitPoint` del bloque anterior como `source_in_ms` del siguiente bloque. Esa suposición solo es válida cuando los bloques siguen en orden cronológico fuente; después de un reorder editorial podía colapsar el bloque siguiente hasta casi `0 ms`, dejándolo invisible en timeline aunque el texto siguiera visible en el transcript. Ahora el split custom sigue cerrando el bloque anterior, pero el bloque nuevo arranca desde su propio `customStartPoint` o desde el `start_ms` real de su primera palabra. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió otra regresión donde parte del transcript de secuencia volvía a desaparecer aunque debía seguir visible como activo o apagado. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) la rehidratación y el remapeo por aproximación temporal habían vuelto a priorizar `sourceClips` antes que `wordIds`; eso reconstruía la secuencia desde los clips activos y podía volver a esconder texto contextual que no estaba representado como clip visible. Ahora `hydrateStoredSequences(...)` y `remapSequenceStateByTimingApproximation(...)` priorizan otra vez `wordIds` completos y dejan `sourceClips` solo como fallback para payloads viejos o incompletos. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió una causa del falso `render final salió mal` que en realidad estaba en la descarga local, no en el encode backend. Se verificó que el MP4 servido por `/api/editor/exports/<id>/download/` era válido (`ffprobe` correcto y decodificación completa con FFmpeg), mientras que el archivo guardado por el flujo de `Download` en el cliente podía quedar en `0 bytes` al pasar por `showSaveFilePicker`, lo que explicaba `moov atom not found` / `Cannot render the file` al abrirlo localmente. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el guardado ahora escribe bytes explícitos (`Uint8Array`) sobre el file handle, verifica el tamaño final del archivo y, si el diálogo del sistema sigue produciendo un archivo vacío o incompleto, hace fallback automático al download normal del navegador usando el blob ya descargado. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se endureció el arranque del export frente a fallos transitorios de conectividad entre frontend y backend. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleExport(...)` ya no cae al primer `Failed to fetch`: ahora reintenta automáticamente hasta 3 veces cuando el POST inicial de export falla por red/proxy, y si finalmente no logra conectar muestra un mensaje explícito sobre backend/proxy de desarrollo en vez del error crudo del browser. La API real de export se verificó aparte con POST directo al backend y respondió `202 pending`, así que esta ronda estuvo enfocada en robustez del cliente, no en la pipeline de render. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se ajustó otra vez la capa de subtítulos del export para acercarla específicamente al tamaño y wrapping del preview. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el `.ass` ya no sale con `WrapStyle: 2` (sin word wrap), sino con wrapping inteligente; además se estrechó la caja útil del subtítulo, se subió el tamaño base por preset, se reforzó el peso visual (`Bahnschrift SemiBold` para `studio-sans`, más escala horizontal/vertical) y se aumentaron outline/shadow. El objetivo directo de esta ronda fue corregir justo el síntoma visto en captura: export en una sola línea y con menos presencia, frente al preview en dos líneas con una negrita mucho más marcada. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: Se corrigió una causa raíz adicional de `exporta la secuencia original aunque el timeline ya cambió`. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) `buildSequenceSourceClips(...)` estaba devolviendo primero los `sourceClips` almacenados en la secuencia y solo derivaba los clips vivos desde `wordIds`/splits/trim si no existía ese snapshot previo. Eso permitía que `buildDraftPayload(...)` enviara al backend una versión vieja del montaje aunque el editor mostrara cortes, trims o reordenamiento nuevos. Ahora, cuando hay `wordsById` disponibles, la serialización deriva primero los clips desde el estado editorial actual y solo usa los `sourceClips` persistidos como fallback. En paralelo, [backend/apps/editor/services.py](backend/apps/editor/services.py) reforzó el overlay `.ass` del export con mayor tamaño base, más outline/shadow y margen inferior algo más cercano al preview para reducir la brecha visual de subtítulos. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-05: Se cerró otra fuente directa de divergencia entre preview y export. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el export ahora prioriza `sourceClips` exactos serializados en el `editor_draft`, usa esos mismos rangos para reconstruir la secuencia exportable y para mapear subtítulos sobre los clips reales en vez de depender de un `zip(...)` frágil contra bloques reconstruidos. En la misma ronda la palabra activa del `.ass` ganó transición real con `\t(...)` para acercarse mejor al `Accent Pop` del preview y el mux final usa `-shortest` para cortar colas donde el último frame podía quedar sostenido o parecer duplicado al final. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: El export de video ahora prioriza hardware encoding NVIDIA cuando el FFmpeg local lo soporta, sin perder robustez. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el selector de encode dejó de devolver una sola receta fija: ahora detecta `h264_nvenc`, arranca por una configuración `VBR HQ` con `CQ`, `AQ`, `lookahead` y preset afinado para equilibrio real entre calidad y velocidad, y si esa ruta falla vuelve automáticamente a `libx264` sin romper el job. El metadata final del export también deja trazado si salió por encoder `hardware` o `software`, para poder ver luego qué camino se usó de verdad. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: La exportación de video empezó a quemar en backend la misma capa editorial que ya se ve en preview. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el render ahora genera un `.ass` temporal por job usando el `editor_draft` activo: respeta `previewSubtitlesEnabled`, `previewMainTitleEnabled`, el texto corregido de `transcriptWordOverrides`, `subtitleHiddenWordIds`, la segmentación real de `buildPreviewSubtitleCues(...)` por bloque y el estilo activo (`Accent Pop` con highlight por palabra, tamaño y fuente). Ese overlay se aplica al final del `filter_complex`, encima del canvas ya compuesto con `fit/fill/split`, para que el MP4 final incluya subtítulos animados y título principal con mucha más paridad frente al preview. En la misma ronda el export XML pasó también a leer el draft override del click actual en vez de depender solo del autosave. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: El modal de export dejó de ser una caja negra durante render. En [backend/apps/editor/services.py](backend/apps/editor/services.py) el export de video ya no usa `subprocess.run(...)` ciego: ahora FFmpeg corre con `-progress pipe:2`, se parsean `frame`, `fps`, `speed`, `out_time_*` y ese estado se persiste periódicamente en `ExportJob.metadata`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la barra del modal ya usa `progress_percent` real y muestra `frame actual`, `fps render`, `velocidad`, `tiempo renderizado` y `duración objetivo`, para que el usuario vea si el render sigue avanzando o se quedó colgado. En la misma ronda el modo `fit` del export dejó de paddear negro y pasó a componer un backdrop blur semitransparente detrás del foreground, acercándose al look del preview actual. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-05: Se endureció otra arista del export contra secuencias transitorias del editor. Algunas secuencias activas del draft usan ids temporales tipo `editorial-approved-...`, y [backend/apps/editor/services.py](backend/apps/editor/services.py) estaba intentando resolverlos como PK numérica al hacer `project.sequences.filter(pk=...)`, lo que disparaba `Field 'id' expected a number`. `_get_export_context(...)` ahora solo consulta DB si el id del draft es realmente numérico; si no, cae sin romper al `active sequence` persistido del proyecto mientras sigue usando el contenido del draft para reconstruir clips exportables. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: Se corrigió la causa del error `No hay clips en la secuencia activa para exportar.` que aparecía aun teniendo una secuencia visible en pantalla. El backend estaba exportando contra `sequence.clips` persistidos en DB, pero el montaje real del usuario seguía viviendo en el `editor_draft` activo. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `_get_export_context(...)` ahora intenta reconstruir los clips del export directamente desde la secuencia activa del draft (`wordIds`, `breakAfterWordIds`, puntos custom) y solo cae a los clips persistidos si no hay draft útil. Con esto el job de export toma la secuencia que el usuario está editando y no una versión vieja o vacía guardada en backend. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: La exportación pasó del flujo síncrono opaco a un job explícito con modal y polling. En [backend/apps/editor/views.py](backend/apps/editor/views.py), [backend/apps/editor/services.py](backend/apps/editor/services.py), [backend/apps/editor\serializers.py](backend/apps/editor/serializers.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) `POST /export/*` ahora crea un `ExportJob` pendiente, lo procesa en un worker de fondo y expone `/api/editor/exports/<id>/` para consultar estado mientras corre. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el click en `Exportar` abre un modal de estado, muestra etapa/backend worker/última actualización y solo ofrece `Download` cuando el job terminó y el archivo existe de verdad. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-05: Se corrigió otra causa real por la que `Exportar` seguía pareciendo roto aun después de cablear la descarga en frontend. El payload estaba devolviendo la URL cruda de `media`, y desde el frontend en desarrollo eso podía fallar al intentar `fetch` directo para guardar el blob. En [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py), [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) ahora cada export expone `/api/editor/exports/<id>/download/`, servido por Django como attachment con `Content-Disposition` y el `output_name` correcto. Con esto la app descarga desde el propio API proxied del editor en vez de depender de la URL directa del archivo media. Validación final en este workspace: `manage.py check` correcto.
- 2026-04-05: El botón `Exportar` dejó de quedarse en un POST silencioso sin descarga visible. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la exportación ahora baja el archivo real al terminar: si el navegador soporta `showSaveFilePicker`, abre directamente el diálogo de guardado con nombre sugerido; si no, cae al download estándar del browser. El nombre propuesto sale de la `label` de la secuencia activa. En [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/services.py](backend/apps/editor/services.py) además se acepta y normaliza `output_name` para que la respuesta del export conserve ese nombre sugerido. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-05: La exportación final pasó a respetar mucho mejor la metadata real del source en vez de caer siempre en un encode genérico por `CRF`. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `probe_video_metadata(...)` ahora extrae también `fps`, resolución, codecs y bitrates desde `ffprobe`, `prepare_video_asset(...)` persiste esa metadata completa, y `export_sequence_as_video(...)` la reutiliza para renderizar con el mismo `frame rate` del original y, cuando existe `video_bitrate`, con rate control apuntado al bitrate del source en lugar del preset fijo anterior. El audio del export también hereda `sample rate`, bitrate y layout base de forma más fiel dentro de un pipeline que sigue reencodeando porque aplica filtros visuales, cortes y subtítulos. Validación final en este workspace: chequeo estático limpio en `backend/apps/editor/services.py`.
- 2026-04-05: Se corrigió una causa raíz del `reorder` que podía resucitar el orden original al reabrir una secuencia. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) la serialización de `sourceClips` estaba resolviendo `sequence.wordIds` otra vez en orden de `start_ms`, así que un bloque movido editorialmente terminaba persistido como si siguiera en cronología fuente. Luego la hidratación priorizaba esos `sourceClips` y reconstruía la secuencia en ese orden viejo. Ahora `buildSequenceSourceClips(...)` respeta el orden actual de `wordIds` al derivar clips persistidos, de modo que el reorder guardado ya no se pierde por una reconstrucción implícita durante la carga. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se afinó otra vez `Accent Pop` para evitar que la palabra activa choque con las vecinas sin volver al layout expansivo anterior. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el highlight mantiene el `scale` simple, pero ahora reserva un padding lateral minimo en layout para abrir un poco de espacio real a cada lado; al mismo tiempo se quitó la animación adicional por keyframes para que el movimiento no se sienta duplicado o repetitivo. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió el experimento de `Accent Pop` que abría demasiado espacio entre palabras y generaba una sensación de doble animación. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el highlight del subtítulo volvió a un pop más simple basado en `transform`, sin desplazar el layout lateral de las palabras vecinas. Se conservaron, eso sí, los nuevos controles persistidos de `tamaño` y `fuente` del subtítulo dentro del menú `Subtitulos`. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: El estilo de subtítulos `Accent Pop` dejó de crecer por superposición y pasó a abrirse paso entre palabras vecinas. En [frontend/src/styles/app.css](frontend/src/styles/app.css) la palabra activa del overlay ahora anima `font-size`, padding y margen lateral en vez de usar solo `transform`, así que al destacar desplaza a las demás hacia izquierda/derecha y también muestra la animación inversa cuando vuelve al estado normal. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) además se añadieron preferencias persistidas para `tamaño` y `fuente` del subtítulo del preview, con opciones visibles dentro del menú `Subtitulos` para ajustar escala (`S/M/L/XL`) y familia tipográfica (`Studio Sans`, `Broadcast`, `Mono`). Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se refinó otra vez la lectura visual de palabras `sin subtítulos` para que se sientan más `ghosted` que marcadas. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el estado `subtitle-hidden` dejó el acento celeste y pasó a un gris más débil, todavía legible pero claramente atenuado, acompañado por un recuadro punteado sutil alrededor de cada palabra para reforzar que sigue en transcript pero queda fuera del output de subtítulos. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: El preview de subtítulos dejó de adelantar texto a través de silencios reales. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `buildPreviewSubtitleCues(...)` ahora corta el cue cuando detecta un gap de al menos `DETECTED_GAP_THRESHOLD_MS` entre palabras, y la selección de `previewActiveSubtitleCue` ya no cae al `nextCue` ni mantiene el último cue fuera de rango. Con esto, si hay un hueco entre frase y frase, ese espacio limpia la pantalla en vez de mostrar por adelantado el subtítulo que todavía no empezó. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se refinó el modo `ocultar solo en subtítulos` para volverlo más usable en edición frecuente. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el atajo principal pasó a ser `V` sobre la selección activa del transcript, manteniendo `Alt + Delete` como alternativa secundaria, y se corrigió además el guard global para que esa variante con `Alt` no quedara bloqueada por el mismo filtro que excluía modificadores. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se retiró el tratamiento repetitivo con badge `CC` y las palabras `sin subs` pasaron a un estado más fantasmal, con menos peso visual pero todavía legible dentro del transcript. Validación final en este workspace: `npm run build` correcto.
- 2026-04-04: Se añadió un modo editorial nuevo para `ocultar solo en subtítulos` sin sacar el texto de la secuencia. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) las secuencias ahora persisten `subtitleHiddenWordIds` junto con el resto del draft, incluyendo hidratación, remapeo y undo/redo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `Alt + Delete` sobre la selección del transcript alterna ese estado, el preview deja de incluir esas palabras en `buildPreviewSubtitleCues(...)`, y el header muestra un contador `sin subs`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) esas palabras ganaron un tratamiento visual propio y un badge `CC` para distinguir `visible en transcript pero invisible en subtítulos` de `apagado total`. Validación final en este workspace: `npm run build` correcto.
- 2026-04-04: Se liberó `Backspace` del circuito de atajos editoriales del transcript para no bloquear la corrección manual de palabras. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) las acciones de desactivar selección o eliminar clip quedaron únicamente en `Delete`, mientras que `Backspace` deja de ser interceptado por el listener global y puede usarse con normalidad dentro del input flotante de `Corregir transcript`. También se actualizaron los textos de ayuda y tooltips para reflejar que la restauración por `Backspace` solo aplica dentro del campo de corrección cuando todo el texto está seleccionado. Validación final en este workspace: `npm run build` correcto.
- 2026-04-04: Se reforzó también la legibilidad visual del texto apagado en el transcript para que `desactivado` no se confunda con `desaparecido`. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el estado `.word-token.inactive` del transcript de secuencia y del transcript de referencia ganó más contraste y un contorno interior suave, de modo que al abrir la página el texto desactivado siga leyéndose claramente aunque no pertenezca al contenido activo/exportable. Validación final en este workspace: `npm run build` correcto.
- 2026-04-04: Se corrigió una segunda causa por la que texto válido desaparecía del transcript de secuencia aunque debía seguir visible como apagado. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) la reconstrucción desde `sourceClips` tomaba solo las palabras activas cubiertas por cada clip, así que cualquier texto situado entre dos clips consecutivos se descartaba por completo al hidratar o remapear la secuencia guardada. Ahora `createSequenceDraftFromClips(...)` reincorpora esas palabras intermedias cuando los clips siguen orden cronológico de fuente y las marca en `inactiveWordIds`, y tanto `hydrateStoredSequences(...)` como `remapSequenceStateByTimingApproximation(...)` vuelven a priorizar esa reconstrucción clip-aware para reparar también secuencias ya persistidas. Validación final en este workspace: `npm run build` correcto.
- 2026-04-04: Se corrigió una pérdida crítica de evidencia editorial al reabrir secuencias con texto apagado. En [frontend/src/pages/dashboardHelpers.js](frontend/src/pages/dashboardHelpers.js) `hydrateStoredSequences(...)` y `remapSequenceStateByTimingApproximation(...)` estaban priorizando la reconstrucción desde `sourceClips`, pero esos clips solo representan el contenido activo/exportable y no conservan las palabras de `inactiveWordIds`. Eso hacía que al reabrir o remapear una secuencia algunas palabras apagadas desaparecieran visualmente del transcript en vez de seguir visibles como evidencia de que existen en la transcripción. Ahora la hidratación y el remapeo priorizan `wordIds` persistidos completos y solo caen a `sourceClips` como fallback cuando faltan palabras guardadas. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió otra causa de `muevo un clip, cierro, reabro y vuelve a su lugar original`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el reorder de bloques dependía demasiado del persist diferido al backend, pero el draft local no se estaba guardando explícitamente al cambiar `sequenceDrafts` o `activeSequenceId`. Si el usuario cerraba o reabría la secuencia antes de que ese persist remoto terminara, podía volver a hidratarse una versión vieja. Ahora el draft local se guarda también cuando cambia la secuencia activa o el orden de bloques, y además se fuerza un guardado del draft antes de `beforeunload`. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: La exportación de video dejó de ignorar el lenguaje visual del editor. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `export_sequence_as_video(...)` ahora lee el `editor_draft` activo, resuelve el `previewAspectPreset` y aplica por clip `fit`, `fill` o `split` con sus `cropRect`, offsets y escala antes de concatenar; también normaliza cada segmento al mismo canvas para reducir glitches al pasar entre clips y usa el draft enviado en el mismo click de export para no depender del autosave. En [backend/apps/editor/views.py](backend/apps/editor/views.py) la ruta `POST /export/video/` acepta `editor_draft` como override puntual, y en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el botón `Exportar` manda el draft visual actual junto con la petición. Validación final en este workspace: `manage.py check` y `npm run build` correctos.
- 2026-04-05: Se corrigió el retardo del preview al pasar de un clip a otro durante reproducción continua. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el salto automático entre bloques usaba `moveToBlock(...)`, pero esa ruta no estaba actualizando `presentedPlaybackBlockId`, así que el canvas seguía aplicando el `fit/fill/split` del clip anterior hasta que el usuario pausaba. Ahora el bloque presentado se actualiza en el mismo momento del salto y el preview cambia de modo visual en vivo mientras la reproducción sigue corriendo. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió la geometría de los badges `B1/B2/B3...` en el transcript de secuencia para que no empujen hacia abajo la primera palabra del bloque. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) cada línea del transcript ahora marca si contiene un inicio de bloque, y en [frontend/src/styles/app.css](frontend/src/styles/app.css) el espacio vertical del badge se reserva a nivel de renglón en vez de meterse como `padding-top` sobre el primer token. Con esto todas las palabras de la oración vuelven a compartir la misma altura visual y el badge queda por encima, como overlay. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se corrigió una causa local de `reordenar clip y al refrescar vuelve atrás` en el editor clásico. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el proyecto podía reabrirse desde un `restore snapshot` local con prioridad alta, pero ese snapshot no se estaba actualizando cuando luego se seguían moviendo clips en timeline. El resultado era que el reorder se veía bien en memoria, pero tras refresh resucitaba un payload anterior desde `localStorage`. Ahora `saveProjectDraft(...)` mantiene sincronizados tanto el draft normal como el `restore snapshot` existente, de modo que los cambios de orden posteriores a una restauración ya no se pierden al recargar. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: El preview del editor clásico recuperó control de volumen propio y el transporte se aplanó todavía más para acercarse al lenguaje `cassette futurism` que se estaba pidiendo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el playerbar de secuencia ahora expone mute, slider de volumen y porcentaje persistido reutilizando la misma clave local del preview compartido, de modo que el audio del preview clásico ya no queda fijo ni desconectado de la preferencia del usuario. En [frontend/src/styles/app.css](frontend/src/styles/app.css) además se rebajaron alturas, radios y densidad del ruler, timecode y botones para que la barra se vea más plana y técnica sin perder clickabilidad. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se rediseñó el playerbar del preview de secuencia para el editor rápido con el lenguaje visual correcto del proyecto: compacto, en dos líneas y con esquinas casi rectas en vez de redondeadas. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el transporte ahora usa controles por `clip` y por `frame` (`clip anterior`, `-10f`, `-1f`, `Play/Pause`, `+1f`, `+10f`, `clip siguiente`) y suma un botón `Cut` que dispara exactamente la misma acción que la tecla `C`. También se retiró el texto accesorio que no aportaba (`Secuencia`, `Bloque N activo`) y la barra quedó reducida a scrubber + timecode + controles útiles. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se rehízo la presentación del transporte con botones casi rectos, más densos y una grilla fija de dos filas. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se consolidó el editor clásico como superficie principal de edición fina para no seguir repartiendo funciones entre dos caminos distintos. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) las aperturas desde secuencias y sugeridas dejaron de hablar de `Slide 4` y pasan a apuntar explícitamente al editor clásico; el shell conserva su `editor dock` como superficie secundaria. En esa misma ronda el preview del editor clásico pasó a reutilizar el mismo playerbar completo del editor embebido, con scrubber de secuencia, saltos `-2s/+2s`, `Inicio`, `Play/Pause`, `Cortar clip`, `Centrar playhead` e `Ir al clip`. También se eliminó una actualización redundante de `presentedPlaybackBlockId` dentro de los ajustes de preview para evitar renders innecesarios durante interacción. Validación final en este workspace: `npm run build` correcto.
- 2026-04-05: Se añadió deduplicación por contenido para no reprocesar el mismo video salvo que el usuario lo pida explícitamente. En [backend/apps/editor/models.py](backend/apps/editor/models.py), [backend/apps/editor/services.py](backend/apps/editor/services.py), [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py), [backend/apps/editor/views.py](backend/apps/editor/views.py) y la migración [backend/apps/editor/migrations/0010_videoasset_source_fingerprint.py](backend/apps/editor/migrations/0010_videoasset_source_fingerprint.py) ahora cada `VideoAsset` guarda `source_fingerprint` (SHA-256), se backfillean fingerprints de assets ya existentes y tanto `POST /projects/` como `POST /append-video/` intentan reutilizar una `part` ya procesada antes de repetir preparación/transcripción. El append además distingue entre duplicado ya presente en el mismo proyecto, reutilización desde otro proyecto y sobrescritura explícita. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) y [frontend/src/styles/app.css](frontend/src/styles/app.css) la biblioteca y el panel `Status` muestran un toggle visible para `Sobrescribir si el mismo video ya fue procesado`, envían `overwrite_existing` al backend y dejan de lanzar polling/transcripción adicional cuando la respuesta ya viene reutilizada y lista. Validación final en este workspace: `manage.py check`, `manage.py makemigrations --check --dry-run`, `manage.py migrate` y `npm run build` correctos.
- 2026-04-05: El append dejó de requerir una recomposición física obligatoria del video para poder editar, scrubbear o cerrar el proyecto multipartes. En [backend/apps/editor/services.py](backend/apps/editor/services.py) se añadieron helpers de offsets globales por parte, resolución `tiempo global -> parte`, composición del transcript activo a partir de transcripts `part` sin necesitar `VideoAsset composite`, y generación virtual de waveform/thumbnails para timeline multipartes. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el cierre de append ya no llama a `build_project_composite_video(...)`, el timeline del proyecto usa assets virtuales cuando hay varias partes y `bootstrap.project.duration_ms` pasa a reflejar la duración global acumulada. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el editor clásico, el preview de selección fuente y el preview editorial pasaron a resolver playback sobre tiempo global y a cambiar de `part` automáticamente al cruzar fronteras, reutilizando los videos y transcripts persistidos como una sola fuente lógica. Validación final en este workspace: `npm run build` y `manage.py check` correctos.
- 2026-04-04: Se endureció la auto-reanudación del append para no volver a disparar trabajo ya en curso solo por abrir o consultar el proyecto. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `_resume_project_processing_if_needed(...)` ya no relanza `append_resume` cada vez que detecta un proyecto multipartes incompleto; ahora solo lo hace si el append no está en `processing`, si realmente quedó `stale`, o si ya cayó en `error/atascada`. Esto evita reanudaciones duplicadas entre procesos o lecturas normales del dashboard, que eran precisamente el tipo de síntoma que podía hacer parecer que "se perdió todo" o que el backend había arrancado otra vez desde cero.
- 2026-04-04: También se dejó sincronizada la card de [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) en `Project List` con el polling vivo de `transcription-status`, para que el listado no siga mostrando `processing` o mensajes viejos cuando el proyecto ya cambió de estado en backend durante la misma sesión.
- 2026-04-04: Se cerró otra fuente de falsa sensación de `reinicio` durante append/recovery. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el modal de estado ya no degrada a `error` solo porque fallen polls intermedios de `GET /transcription-status/` mientras el proyecto sigue `processing`, y también deja de priorizar un `error` local transitorio por encima del estado vivo que llega desde backend. Con esto una corrida larga o recuperada no debería verse como `La retranscripción falló` ni como si hubiera arrancado de cero cuando en realidad el backend seguía avanzando o ya había terminado.
- 2026-04-04: Se corrigió un bloqueo real del cierre de append en proyectos multipartes largos. En [backend/apps/editor/views.py](backend/apps/editor/views.py) la finalización del append ya no depende del `preview seekable` del video `composite`, porque esa codificación podía quedar a medias, dejar varios `.tmp.mp4` y mantener el proyecto pegado en `processing` con mensaje engañoso de `Recomponiendo transcript activo`. Ahora el worker solo exige audio + transcripts para cerrar el append, mueve la regeneración del preview compuesto a una tarea best-effort en segundo plano y, además, si faltan transcripts `part` de partes anteriores, los retranscribe automáticamente antes de recomponer el transcript activo. Con esto proyectos como el 24 dejan de caer en un estado imposible donde solo la parte nueva tenía transcript y el append ya no puede quedar clavado por el preview del compuesto.
- 2026-04-04: Se aclaró la UI cuando el proyecto sigue `processing` en backend para que no parezca que un botón editorial relanzó una retranscripción. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el modal de proceso ya toma su título de la etapa real (`current_stage_label`) en vez de caer siempre en `Retranscribiendo proyecto`, y en takeover las acciones editoriales de revisión que mutan estado (`Ajustar rango`, `Restaurar rango`, `Descartar`, `Aprobar`) quedan bloqueadas mientras el proyecto siga procesando en segundo plano.
- 2026-04-04: Se eliminó también la banda superior de `Revisión editorial en el editor real` cuando el editor clásico entra en takeover desde library. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) esa franja ya no se renderiza en takeover, `Salir al selector` cierra además la revisión activa antes de volver a `Secuencias`, y las acciones clave de revisión (`Ajustar rango`, `Ver análisis IA`, `Restaurar rango`, `Descartar`, `Aprobar corte`) se recolocaron en formato compacto dentro del header del preview.
- 2026-04-04: El takeover al editor clásico quedó más estricto para no arrastrar módulos ajenos. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el modo `editorTakeoverMode` ahora oculta el `workflow-command-deck` y toda la cortina superior de `Script principal` + barra de secuencias/sugeridas, dejando visible solo el editor real de transcript, preview y timeline. La salida también se movió junto a `Undo/Redo` con un botón `Salir al selector` que devuelve directo al slide `Secuencias`.
- 2026-04-04: Abrir una secuencia desde library ahora entra al editor clásico como vista takeover en vez de empujar al slide 4 modular. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) las acciones de `Abrir rango`, `Abrir secuencia` y `Abrir editor clásico` pasan a `activeView="editor"` guardando un slide de retorno, y la cabecera del editor clásico sumó `Volver a la app` para regresar al shell sin cerrar el proyecto. Con esto la edición fina usa por defecto la superficie más rápida y el shell completo desaparece mientras se trabaja la secuencia.
- 2026-04-04: Se redujo una diferencia importante de “inmediatez” entre el editor clásico y el modular durante scrub en timeline/preview. La causa principal estaba en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx): el gesto de scrub seguía pasando por `syncPreviewToSequenceMs(...)`, una ruta pensada para saltos/selecciones discretas que en cada `pointermove` hacía demasiado trabajo (`stopPlayback`, cambios de bloque activo/seleccionado, `setPlaybackState`, `setCurrentSequenceTimeMs`, `video.currentTime`). Eso volvía el scrub del modular más pesado y fácil de sentir como congelado. Ahora existe una vía rápida `scrubPreviewToSequenceMs(...)` para arrastres, que solo mueve playhead/video y actualiza selección cuando realmente cambia el bloque, manteniendo la ruta completa para acciones discretas donde sí hace falta recomponer todo el estado.
- 2026-04-04: El preview modular de slide 4 recuperó también los controles de toolbar que todavía estaban solo en el editor clásico. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el panel ahora monta el mismo selector `Modo clip`, el menú de estilos de `Subtitulos` y el toggle de `Titulo principal` que ya existían en la superficie clásica, reutilizando el mismo estado y la misma lógica de preview. Con esto la paridad entre ambos previews deja de depender de “dos barras distintas” y el modular ya expone las funciones que seguían faltando en la zona superior marcada por el usuario.
- 2026-04-04: Se corrigió otro bloqueo de `Espacio` que aparecía después de scrub o reacomodos dentro del editor. La causa no estaba ya en la timeline local, sino en [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx): la preview compartida del shell de biblioteca seguía registrando su listener global de teclado sobre `window` aunque esa preview estuviera inactiva. Como ese listener corre en captura, podía tragarse la barra espaciadora antes de que llegara al handler del editor de secuencia actual. Ahora solo instala ese listener cuando `isActive` es `true`, así que la preview de slide 2 deja de interferir con `play/pause` del editor cuando el usuario está trabajando en slide 4 o en el workbench clásico.
- 2026-04-04: Se reforzó el atajo de `Espacio` después de clicks directos sobre transcript y timeline. El problema no era la lógica de play/pause en sí, sino que al quedar el foco sobre botones concretos (`word-token`, `workflow-text-block`, `workflow-clip-block`) el comportamiento dependía demasiado del listener global. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) esos elementos enfocados ahora manejan `Space` explícitamente como toggle de reproducción, de modo que tras click en una palabra del transcript o en un bloque de la timeline la barra espaciadora vuelve a arrancar/pausar el preview sin ambigüedad.
- 2026-04-04: Se habilitó scrub directo también sobre la barra verde y la pista de thumbnails del timeline. En [frontend/src/components/WorkflowTimelinePanel.jsx](frontend/src/components/WorkflowTimelinePanel.jsx) y [frontend/src/components/WorkflowTimelineClipLane.jsx](frontend/src/components/WorkflowTimelineClipLane.jsx) los bloques de texto/video ya no cortan el `pointerdown` que necesita el viewport para iniciar scrub; ahora ese gesto puede atravesar la superficie de la pista y reutilizar el mismo handler que ya funcionaba en el ruler y en el audio. Se mantuvieron aislados solo los subcontroles que sí deben capturar el puntero por su cuenta, como trims, fundidos y el lift-tab de reorder.
- 2026-04-04: Se corrigió una carrera concreta entre `click en transcript` y `Play` del preview de secuencia. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el efecto que resincroniza el preview primario cuando el editor queda en `idle` podía seguir vivo justo después de mover el playhead desde el transcript y, si el usuario apretaba `Play` enseguida, ese efecto viejo todavía llegaba a ejecutar `pause()` sobre el `<video>` ya relanzado. Ahora esa resincronización captura la `session` idle y vuelve a comprobar `mode/session` antes de tocar o pausar el video, así que el click en texto puede seguir reajustando el preview sin matar la reproducción inmediata del playbar.
- 2026-04-04: Se revisó la sospecha de `duplicación de preview/FFmpeg` al reproducir desde slide 4. El hallazgo principal no fue una nueva generación backend por cada `Play`: [backend/apps/editor/views.py](backend/apps/editor/views.py) sigue sirviendo el preview existente o, si falta, el source directo sin relanzar codificación dentro del request. El problema real estaba del lado frontend: [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) mantenía videos auxiliares ocultos del preview clásico reproduciéndose en paralelo al principal, y el preview compartido de slide 2 seguía montado aunque la slide no estuviera activa. Se corrigió dejando los auxiliares como frames pausados sincronizados y añadiendo `isActive` en [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx) para pausar el preview compartido fuera de slide 2; además [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js) dejó de escribir estado redundante en cada tick del playhead para reducir el loop `Maximum update depth exceeded` visto en runtime.
- 2026-04-04: Slide 4 del library-shell recuperó utilidades básicas del editor real. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el click normal sobre palabras del transcript ya mueve playhead/preview/timeline al inicio de la palabra en vez de exigir `Alt+click`, los gaps del transcript también sincronizan el playhead de forma consistente, y el preview modular añadió una franja de transporte compacta con scrubber de secuencia, saltos `-2s/+2s`, `Play/Pause`, `Inicio`, `Cortar clip`, `Centrar playhead` e `Ir al clip`. En la misma ronda se endureció el teardown del drag global de timeline escuchando también `pointercancel` y `blur`, para reducir casos donde el scrub quedaba pegado tras perder la captura del puntero. En [frontend/src/styles/app.css](frontend/src/styles/app.css) además se compactó el header de slide 4, se retiró la nota informativa redundante y el encabezado pasó a priorizar el nombre del proyecto sobre la etiqueta genérica `Slide 4`.
- 2026-04-04: Se corrigió otra regresión de restauración al refrescar el dashboard. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la sesión ya no vuelve a abrir automáticamente el editor clásico ni el slide 4 al recargar; ahora normaliza cualquier restore hacia el library-shell, persiste un slide navegable (`project`/`sequences`) y además re-sincroniza el scroll físico del shell con el slide restaurado para evitar quedar en una pantalla negra o fija sin poder volver por wheel al resto de slides.
- 2026-04-04: El library-shell pasó de 2 a 4 slides reales. En [frontend/src/pages/dashboardShellHelpers.js](frontend/src/pages/dashboardShellHelpers.js) se declararon nuevos paneles/layouts para `Secuencias creadas`, `Secuencias sugeridas`, `Editor` y `Timeline`; y en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el shell ahora monta slide 3 como hub dockable de secuencias y slide 4 como editor modular con transcript, preview y timeline reutilizando la lógica existente del workbench. La selección de una sugerida en slide 3 abre además un modal de detalle con CTA para abrir directamente esa revisión en slide 4.
- 2026-04-04: Reajusté el `slide intent` para priorizar otra vez los divisores internos del slide. Los `dock-split-divider` y rails entre módulos vuelven a iluminarse por etapa: naranja en el primer scroll y un naranja más caliente/rojizo desde el segundo, manteniéndose así sin volver a teñir el interior de los módulos.
- 2026-04-04: Refiné otra vez el `slide intent` del library-shell para que los módulos no cambien su superficie durante el gesto. Los paneles internos ya no reciben `background` ni `glow` propios en los pasos 1/2/3; la señal quedó limitada a contornos y separadores del shell para mantener el contenido visual del módulo intacto.
- 2026-04-04: Los estados visuales del library-shell dejaron de depender de naranjas/ambar/verde hardcodeados dentro del bloque de transición. En [frontend/src/styles/app.css](frontend/src/styles/app.css) la señal de slide intent ahora consume tokens semánticos del theme (`--theme-active-*` / `--theme-intent-*`) y prioriza contornos, rails y sombras sobre rellenos tintados de panel, para que futuros themes puedan recolorear el comportamiento sin reescribir reglas específicas.
- 2026-04-04: Library-shell wheel slide navigation now runs on a two-step contract instead of three. The shell quantizes `wheel` input into normalized notch-like clicks (including cases where one browser event contains more than one physical notch), so mouse-wheel movement maps more faithfully to staged progress: first click arms the glow, second click commits the animated snap.
- 2026-04-04: Made library-shell slide intent visible even before the target slide enters view. Added shell-level amber overlays tied to `step 1/2/3`, slowed the snap traversal easing, and mirrored the staged glow onto the currently visible slide surfaces so the user sees the build-up before the animated scroll starts.
- 2026-04-04: Strengthened pre-transition visuals for the library shell. The first and second wheel steps now animate the target slide's panel headers and panel bodies directly, so the dark surfaces visibly "wake up" before the third-scroll snap commit starts.
- 2026-04-04: Expanded the library-shell transition glow beyond the outer slide frame. Steps 1 and 2 now tint the target slide's dark surfaces (panels, dock dividers, empty states, and stage background), while step 3 keeps those surfaces lit with a short commit animation for the entire snap transition before they fade back out.
- 2026-04-04: Moved library-shell wheel interception from React `onWheelCapture` to a native non-passive `wheel` listener on the shell itself, and added `overscroll-behavior-y: none` so the first wheel delta cannot slip through into native slide movement before staged intent logic runs.
- 2026-04-04: Adjusted library-shell wheel staging to a literal three-scroll contract: first scroll only arms the target slide with a soft separator/background glow, second scroll intensifies it to orange without moving the shell, and only the third scroll commits the snap transition.
- 2026-04-04: Simplified library-shell wheel slide navigation from a reversible multi-step scroll experiment to a two-phase model: `armed` keeps the current slide fully anchored while the next slide glows as a pre-confirmation cue, and `committed` runs one full automated transition with no partial drift or snap-back.
- 2026-04-04: Replaced native `scrollIntoView`/`scrollTo(..., smooth)` slide transitions in the library shell with a dedicated `requestAnimationFrame` easing pass. This makes the 30% intermediate drift and the final snap more deterministic, and adds compositor hints (`contain`, `translateZ(0)`, `will-change`) so the freeze/amber overlay animates with less repaint pressure.
- 2026-04-04: Refined wheel slide-transition staging in the library shell. Step 1 now acts like a visual freeze warning with stronger amber illumination on the target slide and separator edge, step 2 keeps the 30% drift without native snap fighting it, and wheel momentum inside the cooldown no longer wipes the staged intent prematurely.
- 2026-04-04: Hardened `SharedPreviewSurface` against self-inflicted replay glitches in slide 2. During normal playback, parent `currentTimeMs` echoes from the same `<video>` are no longer treated as fresh seek commands unless drift is materially large, which avoids micro-restarts around transcript gaps/silences.
- 2026-04-04: La `Project List` dejó de montar previews vivos por card. En [backend/apps/editor/services.py](backend/apps/editor/services.py) se añadió una miniatura estática cacheada por video (`card_preview.png`) generada una sola vez con FFmpeg y reutilizada luego como archivo de imagen; [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py) expone esa `thumbnail_url`, y [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) reemplazó el `<video>` de cada card por `<img loading="lazy">`. Con esto la slide 1 deja de abrir múltiples instancias de video solo para listar proyectos: la card queda como foto estática y el preview reproducible real se reserva para la slide activa donde sí hace falta navegar el transcript o editar secuencias.
- 2026-04-04: Se eliminó una fuente importante de `ffmpeg` duplicado disparado por la propia UI. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `build_bootstrap_payload(...)` ya no precalienta previews seekable al abrir un proyecto, y `GET /video-preview/` dejó de generar previews bajo demanda desde una request normal del navegador: ahora sirve el preview existente si ya está en disco y, si no existe o quedó viejo, hace fallback al archivo fuente en vez de lanzar una nueva codificación dentro del request. En paralelo, [backend/apps/editor/services.py](backend/apps/editor/services.py) dejó de ejecutar la sonda de salud por decodificación sobre cada request de preview, porque eso también multiplicaba procesos `ffmpeg` desde la navegación del listado. Con esto abrir `Projects` no debería volver a inundar la máquina con varios `ffmpeg.exe` simultáneos solo por mostrar cards o abrir el slide 2.
- 2026-04-04: El flujo de append dejó de depender del preview seekable del `composite` para poder revisarse al terminar. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `build_bootstrap_payload(...)` ahora expone por cada `source_part` una `video_url` propia y offsets globales (`global_start_ms`, `global_end_ms`), y `GET /video-preview/` acepta `video_id` para servir el preview de una parte concreta; además, cuando se consulta sin `video_id` sobre un proyecto multipartes, el endpoint ya no intenta forzar el compuesto como preview base de la card. En [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx) el preview compartido pasó a soportar una timeline virtual multipartes: resuelve `tiempo global -> parte + tiempo local`, cambia de archivo al cruzar el borde entre partes y puede seguir reproduciendo/scrubbeando el proyecto sin necesitar un MP4 compuesto reproducible. Con esto el append puede cerrarse y revisarse minimizando la dependencia en FFmpeg para previsualización.
- 2026-04-04: Se corrigió una trampa operativa del append que podía dejar proyectos aparentando una cancelación eterna. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `POST /transcription-control/` ya no acepta `pause/cancel/resume` sobre contextos `append-processing` o `media-preparation`, porque ese endpoint era exclusivo de transcripción pero estaba mutando igual el `processing_message` de corridas de append y dejaba mensajes como `Cancelación solicitada...` aunque el worker real estuviera componiendo fuente/transcript. En la misma ronda se añadió una finalización defensiva del append: si al reabrir o consultar un proyecto el backend detecta que ya existen todas las partes con transcript local y la fuente compuesta está lista, termina de reconstruir el transcript compuesto faltante y promueve el proyecto a `ready` sin volver a rehacer FFmpeg ni a retranscribir partes ya cerradas.
- 2026-04-04: La composición de fuente continua por append ahora intenta una vía rápida sin reencode cuando las partes son compatibles. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `build_project_composite_video(...)` primero inspecciona los streams reales con `ffprobe` y, si todas las partes comparten firma compatible de video/audio (`h264/aac`, resolución, pixel format, frame rate, sample rate y canales), concatena con demuxer `concat` + `-c copy` en vez de rehacer todo con `libx264`. Si esa vía rápida falla o las partes no son equivalentes, el backend cae automáticamente al reencode anterior como fallback seguro. En el caso del proyecto 24 se confirmó que ambas partes son compatibles, así que las próximas recomposiciones de este tipo deberían bajar drásticamente el tiempo de `Componer fuente continua`.
- 2026-04-04: Se corrigió una lectura engañosa del progreso durante `append-compose-source`. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el heartbeat genérico seguía empujando `processing_progress` hasta `99%` aunque el proyecto todavía estuviera dentro de `Componer fuente continua`, porque solo conocía topes amplios por rango y no por etapa real. Eso hacía que una recomposición larga con FFmpeg pareciera bloqueada en `99%` aunque el worker siguiera vivo reencodeando la fuente compuesta. Ahora el heartbeat limita el avance al tope de la etapa efectiva (`append-compose-source` se queda bajo `88%` hasta que termine) y el mensaje de esa fase deja explícito que la composición con FFmpeg puede tardar varios minutos en videos largos.
- 2026-04-04: El modal de retranscripción dejó de caer en falso `error` ante fallos transitorios del polling mientras el backend seguía vivo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `pollTranscription(...)` ahora tolera varios errores consecutivos de `GET /transcription-status` antes de rendirse, registra el incidente en la consola del modal y mantiene la sesión en modo `saving` con `Reintentando...` mientras el proyecto o la sesión local sigan marcados como activos. Con esto ya no debería aparecer `La retranscripción falló / No se pudo consultar la transcripción` solo porque una consulta intermedia falle aunque el worker continúe procesando en segundo plano.
- 2026-04-04: El modal también dejó de quedarse pegado a un `error` local viejo cuando el mismo proyecto sigue reportando `processing` desde backend. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) una sesión local `error` ahora se considera recuperable si apunta al proyecto cargado y ese proyecto sigue en `processing`; en ese caso el modal vuelve a tomar como fuente de verdad el estado vivo del backend en vez de seguir mostrando un fallo ya obsoleto.
- 2026-04-04: Se corrigió otra regresión de la reapertura pasiva de proyectos en proceso. Al restaurar la sesión tras refresh, [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) volvía a crear una sesión local de modal para `opened-project-monitor`, y eso podía reabrir el popup o mostrar métricas de tiempo engañosas aunque la corrida hubiera empezado mucho antes en backend. Ahora la restauración pasiva sigue monitoreando el proyecto sin capturar una sesión local de modal ni inventar `tiempo restante / termina aprox.` desde cero; esos números solo se muestran cuando la corrida fue iniciada o seguida activamente en esta pestaña.
- 2026-04-04: Se corrigió el entorno de transcripción para usar GPU real en vez de CPU. La máquina ya tenía una NVIDIA RTX 3070 Ti Laptop y drivers correctos (`nvidia-smi` visible), pero el `venv` estaba montado con `torch 2.10.0+cpu`, así que `stable-ts` forzaba `FP32` en CPU aunque `transcription_device` estuviera en `auto`. Se reemplazó PyTorch por la build CUDA (`torch 2.10.0+cu128` y `torchaudio 2.10.0+cu128`), se dejó el `WorkspaceSettings.transcription_device = cuda` y se relanzó la reparación del proyecto 24 para que la transcripción de la parte 2 corra de verdad sobre GPU. En [backend/requirements.txt](backend/requirements.txt) además se alinearon versiones y quedó documentado que, en Windows con NVIDIA, la instalación debe salir del índice CUDA de PyTorch para no volver a caer en la variante `+cpu`.
- 2026-04-04: Se corrigió una lectura falsa de `append terminado` en el monitor de proceso. Después del cambio a append no destructivo por partes, [backend/apps/editor/views.py](backend/apps/editor/views.py) seguía dejando que `_resume_project_processing_if_needed(...)` inspeccionara artefactos de la fuente vieja ya preparada y marcara el proyecto como `ready` con `Video listo y audio separado. Falta transcribir.` aunque la parte nueva todavía no hubiera terminado de transcribirse ni de recomponerse dentro del proyecto. La corrección separa el append como contexto propio (`append-processing`), hace que el snapshot de artefactos mire la parte nueva o la fuente compuesta según la etapa real, y desactiva para ese flujo la autopromoción a `ready` usada por la preparación inicial normal. Además el panel de etapas ahora reconoce explícitamente `Preparar parte nueva`, `Transcribir parte nueva`, `Componer fuente continua` y `Recomponer transcript activo`, para que el reporte deje de mostrar la línea de tiempo equivocada del pipeline genérico.
- 2026-04-04: El append de video dejó de ser destructivo y pasó a operar por `partes` procesadas. En [backend/apps/editor/models.py](backend/apps/editor/models.py), [backend/apps/editor/services.py](backend/apps/editor/services.py), [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py), [backend/apps/editor/views.py](backend/apps/editor/views.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) ahora cada `VideoAsset` puede representar una `part` individual o una fuente `composite`, y los transcripts distinguen entre piezas individuales y transcript activo compuesto. El flujo nuevo de `POST /append-video/` ya no borra el transcript anterior: crea una nueva parte, la procesa con el mismo pipeline del alta normal (metadata, audio, preview, transcripción), recompone la fuente continua del proyecto en orden directo y genera un transcript activo combinado a partir de las partes ya transcritas. Para evitar arrastrar referencias editoriales incompatibles, se limpia solo el estado dependiente del transcript compuesto (`editor_draft`, anotaciones, sugeridas), pero se conservan las partes originales y sus transcripts individuales. En paralelo se añadió `POST /api/editor/projects/<pk>/extract-part/`, que permite separar cualquier parte fuente en un proyecto nuevo sin destruir el proyecto original.
- 2026-04-04: El frontend quedó alineado con el append no destructivo y la reversibilidad local. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el append ahora guarda un `save state` local `before-append-video` cuando el proyecto objetivo está abierto, el panel `Status` del proyecto activo muestra una lista de `Partes fuente` con duración/estado y cada parte puede separarse con `Separar como proyecto`. Ese mismo panel ganó `Restaurar save state` para volver al estado local guardado antes del append en este navegador. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se añadieron estilos compactos para la nueva lista de partes. Validación final: `npm run build`, `manage.py check`, `manage.py migrate --plan` y `manage.py migrate` quedaron correctos en el workspace actual.
- 2026-04-04: Se movió la acción de anexar otra parte de video al flujo visible de slide 1 y se compactó otra vez `Project List`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el append dejó de depender exclusivamente del `activeProjectId` abierto en el shell: ahora puede dispararse contra cualquier proyecto desde su propia card, usando un target explícito antes de abrir el selector de archivo. También se reubicó el input oculto de upload para que la acción funcione aunque el panel `Status` no esté montado. En la misma ronda cada card del listado ganó un botón visible `Añadir parte` dentro del bloque de preview, mientras se recortó información redundante: salieron los hints de interacción, se eliminó la fecha del meta, la descripción pasó a una sola línea y la actividad quedó como una banda mucho más compacta. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se ajustaron overlay, chips, botones y altura de copy para que las cards entren más densas sin perder legibilidad.
- 2026-04-03: Se añadió un MVP para anexar otro bloque de video al final de un proyecto existente y tratarlo luego como una sola fuente continua. En [backend/apps/editor/views.py](backend/apps/editor/views.py), [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py), [backend/apps/editor/services.py](backend/apps/editor/services.py) y [backend/apps/editor/urls.py](backend/apps/editor/urls.py) ahora existe `POST /api/editor/projects/<pk>/append-video/`: el backend almacena temporalmente el nuevo upload, concatena el video adicional al final del video principal con FFmpeg, reemplaza la fuente base del proyecto, limpia transcript / sugeridas / logs / secuencias derivadas y vuelve a preparar WAV + preview seekable para que la siguiente transcripción vea todo como una sola línea de tiempo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el proyecto activo ganó el botón `Añadir video al final`, con confirmación cuando la operación va a invalidar transcript o secuencias ya derivadas. La subida reutiliza el mismo modal de proceso para seguir la unión y la nueva preparación en background. Limitación deliberada del MVP: al cambiar la fuente base del proyecto, la edición derivada anterior se resetea para evitar conservar timestamps inválidos sobre una timeline diferente.
- 2026-04-03: Se corrigió un crash de frontend introducido en la estabilización del modal de proceso. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la rutina `pinLibraryShellToOverview` había quedado declarada con `useCallback(...)` después de un `useEffect` y de handlers que ya la referenciaban, provocando `ReferenceError: Cannot access 'pinLibraryShellToOverview' before initialization` al montar la vista. Se reordenó su declaración para que exista antes de cualquier uso y la app vuelva a cargar con el anclaje del modal al slide 1 intacto.
- 2026-04-03: Se añadió un bloqueo explícito para que abrir el modal de procesamiento no vuelva a empujar el shell al slide 2. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) `handleOpenTranscriptionStatusModal()` y las aperturas automáticas del modal ahora llaman a una rutina que ancla el `libraryShell` al overview (`slide 1`) con scroll inmediato, y mientras el modal permanezca abierto se desactiva tanto la detección automática del slide visible como la navegación por wheel entre slides. Con esto el modal de procesamiento ya no depende de que el shell siga `closest section`, sino que vive arriba del slide 1 hasta que el usuario lo cierre.
- 2026-04-03: Se cambió el flujo de proyecto nuevo para que el modal de procesamiento deje de pelear con el shell global y se mantenga en el slide 1 durante toda la corrida. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la creación/preparación/transcripción de un proyecto nuevo ya no hace `openProject(...)` en mitad del pipeline ni hidrata `bootstrap`, secuencias, palabras u OpenAI trace del shell mientras la corrida sigue viva; en su lugar, el seguimiento queda completamente en la sesión local del modal y en la lista de proyectos. Con esto el backend puede seguir preparando y transcribiendo, pero el frontend no empuja al usuario al slide 2 ni mezcla el modal con el proyecto activo visible. Al terminar, el proyecto queda actualizado en la lista para abrirlo manualmente cuando el usuario quiera.
- 2026-04-03: Se corrigió otro foco de `glitch` entre el modal de proceso y el shell de `Projects` cuando una corrida seguía viva mientras el frontend reabría el proyecto automáticamente. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la visibilidad del modal ya no depende del cálculo derivado `busy/sessionPinned`, sino del estado explícito de apertura, para evitar que pequeños baches del polling o cambios de fuente entre sesión local y bootstrap lo monten/desmonten visualmente. En la misma ronda `Cerrar` dejó de quedar bloqueado durante `processing`: al cerrarlo se suprime la reapertura automática, pero se conserva la sesión para poder volver a abrirlo sin perder consola ni contexto. Además, las aperturas automáticas de proyecto usadas por restauración pasiva o por continuación del pipeline ya no fuerzan `scrollLibraryShellSection("project")`, así que el shell deja de empujarte solo al slide 2 mientras la operación sigue corriendo en segundo plano. Como ayuda extra para el caso del `81%`, el encabezado del modal ahora distingue `global` frente a `etapa`, mostrando etiquetas del tipo `81% global · 98% etapa` para no confundir una etapa todavía viva con un bloqueo duro.
- 2026-04-03: Se añadió control operativo real sobre transcripciones en curso y se compactó otra vez el modal global de proceso. En [backend/apps/editor/views.py](backend/apps/editor/views.py) ahora existe `POST /api/editor/projects/<pk>/transcription-control/` con acciones `pause`, `cancel` y `resume`; el worker de transcripción coopera con esas órdenes y, como `Project.status` no tiene estado `paused`, una pausa o cancelación deja el proyecto en estado reanudable con mensaje explícito para poder retomarlo sin recrearlo. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el modal ganó botones reales `Pausar`, `Cancelar`, `Reanudar`, `Copiar reporte` y `Cerrar`, y además se comprimió moviendo consola, preview, etapas y artefactos a secciones plegables. En [frontend/src/styles/app.css](frontend/src/styles/app.css) se redujeron ancho, alto, paddings, tipografías y alturas de scroll para que el modal deje de desperdiciar espacio útil. Durante la validación final, el backend vivo del proyecto 24 llegó a mostrar un estado transitorio `Reanudando transcripción tras reinicio...`, pero en el siguiente muestreo volvió a `Transcribiendo con stable-ts (large-v3)...` y avanzó de `47%` a `49%` con `worker_active: true`, así que el síntoma más reciente no correspondía a un bloqueo duro sostenido.
- 2026-04-03: Se corrigió la reapertura intrusiva del modal de proceso al refrescar la app. La rehidratación automática del último proyecto en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) estaba entrando por `openProject(...)`, detectaba proyectos `processing` o incompletos y volvía a montar el portal como si fuera una corrida interactiva nueva; además la regla `busy => visible` hacía que el popup regresara aunque el usuario solo estuviera restaurando el shell. Ahora la restauración pasiva del último proyecto suprime el auto-popup del modal: el frontend sigue haciendo polling y alimentando el panel `Status`, pero no monta el portal hasta que el usuario lo abra manualmente o lance una corrida nueva desde ese navegador. En paralelo, el panel `Status` ganó `Abrir modal` y `Copiar reporte` para seguir inspeccionando el proceso sin depender de ese popup automático.
- 2026-04-03: Se corrigió otra causa de falso `atasco` en retranscripciones largas con `stable-ts`. El worker de transcripción en [backend/apps/editor/views.py](backend/apps/editor/views.py) ya tenía heartbeat, pero dejaba de refrescar `updated_at` al alcanzar `98%` y además podía inflar el progreso dentro de `Transcribir` hasta parecer que ya iba por `Guardar palabras` aunque todavía no hubiera terminado esa etapa. En paralelo, [backend/apps/editor/services.py](backend/apps/editor/services.py) persistía segmentos/palabras con `bulk_create(...)` monolítico, así que una corrida larga podía pasar varios minutos sin una sola señal nueva justo al final. Ahora el heartbeat sigue tocando `updated_at` aunque el progreso ya esté en el tope de su etapa real, el tope se limita por fase (`Transcribir` no salta artificialmente a `98%`) y la persistencia de segmentos/palabras avanza por lotes con mensajes/progreso reales (`x/y`) para que el modal refleje trabajo vivo hasta el final. También `GET /transcription-status` marca `stale` y reintenta la reanudación en la misma consulta, evitando el estado intermedio donde el backend decía `can_auto_resume: true` pero todavía respondía `resume_info: null` en ese primer poll.
- 2026-04-03: Se estabilizó el modal global de proceso para eliminar un glitch visual donde parecía `aparecer y desaparecer` cuando entraban updates nuevos del backend. La causa era que [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) mezclaba dos fuentes de verdad distintas: el estado global `statusMap.transcription.type` y el estado derivado de la sesión local del modal / `bootstrap.project`. Con cada poll podían desalinearse uno o dos renders y el portal cambiaba tono/visibilidad justo en medio del avance. Ahora la visibilidad, el `busy`, el `kicker`, la barra de progreso y el guard de cierre del modal usan la señal derivada estabilizada (`transcriptionStatusTone` + sesión pinneada) en vez de depender de transiciones momentáneas del `statusMap` global.
- 2026-04-03: Se afinó la recuperación de transcripciones interrumpidas tras reinicio para que no vuelvan a caer en el falso `La transcripcion anterior quedo atascada...` sin intentar continuar. En [backend/apps/editor/views.py](backend/apps/editor/views.py) la reanudación ya no espera únicamente al timeout `stale`: si un proyecto sigue en `processing`, no tiene transcript materializado, el audio ya está listo y no hay worker activo en el proceso actual durante un margen corto, el backend lo considera recuperable y relanza la transcripción automáticamente al consultar `bootstrap` o `transcription-status`. Además, cuando una corrida sí se marca como atascada, ahora conserva el progreso previo en vez de volver siempre a `0%`, para que el modal no pierda por completo la pista de cuánto había avanzado antes del corte. Durante la validación final, el `GET /transcription-status` vivo del proyecto 24 volvió a dejarlo en `processing` y `worker_active: true`, con etapa `Transcribir` usando `stable-ts (large-v3)`.
- 2026-04-03: Se completó el circuito de reanudación para pipelines de proyecto interrumpidos. En [backend/apps/editor/views.py](backend/apps/editor/views.py) ahora existe un registro de workers activos y una vía de auto-recuperación al consultar `bootstrap` o `transcription-status`: si un proyecto quedó en preparación/transcripción a medias por reinicio, el backend distingue entre trabajo todavía vivo, trabajo realmente `stale`, y artifacts ya terminados en disco. Con eso puede 1) no duplicar workers cuando el proceso real sigue corriendo, 2) relanzar preparación/transcripción solo si quedó realmente colgada y 3) cerrar automáticamente la preparación si detecta que el WAV y el preview ya están listos aunque el worker original se haya perdido. En paralelo, [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ahora abre automáticamente el modal detallado al abrir un proyecto incompleto y, si el proyecto quedó en `ready` pero aún sin transcript (`Video listo y audio separado. Falta transcribir.`), continúa automáticamente la transcripción pendiente con la configuración activa del workspace. Durante la verificación, el proyecto 24 fue recuperado hasta `ready`, luego relanzado por API con `stable-ts + large-v3 + es`, y quedó otra vez en `processing` (`Verificando audio extraido del proyecto...`) ya dentro de la corrida real de transcripción.
- 2026-04-03: Se detectó la causa raíz del falso `atasco` en proyectos nuevos pesados: el worker de preparación inicial podía pasar varios minutos dentro de `ffmpeg` extrayendo audio o generando el preview seekable sin volver a tocar `updated_at`, así que [backend/apps/editor/views.py](backend/apps/editor/views.py) lo declaraba `stale` a los 120s aunque el proceso seguía vivo y, de hecho, mantenía el MP4 bloqueado en Windows. Ahora `_run_project_media_preparation_job(...)` tiene heartbeat propio mientras corre la preparación pesada y `_project_processing_is_stale(...)` usa un umbral mucho más largo para el contexto `media-preparation`, evitando falsos `error` mientras `ffmpeg` sigue trabajando. Durante la verificación se confirmó un `ffmpeg.exe` activo sobre `media/videos/20260325055457-1.mp4 -> media/videos/preview/project_24/video_25_seekable.mp4`, y el proyecto 24 se volvió a marcar manualmente como `processing` con `Audio extraido. Generando preview seekable...` para reflejar el estado real del worker aún en curso.
- 2026-04-03: Se corrigió la lectura del modal de proceso cuando falla la preparación inicial de un proyecto nuevo. Aunque el backend ya diferenciaba `preparación inicial atascada` de `transcripción atascada`, el payload de etapas en [backend/apps/editor/views.py](backend/apps/editor/views.py) seguía cayendo en la línea de tiempo genérica de transcripción (`Guardar segmentos`, `Guardar palabras`, `Listo`) porque, al marcar el error, el mensaje original quedaba reemplazado y el clasificador perdía el contexto. Ahora esos casos renderizan una secuencia coherente de `Preparar proyecto`, `Extraer audio`, `Generar preview` y `Pendiente de transcribir`, con la etapa real marcada como fallida. En paralelo, [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) sumó un botón `Copiar reporte` dentro del modal para exportar estado, etapas, consola y preview parcial en texto plano y poder pegarme todo el contexto exacto de la corrida. Durante la verificación, el proyecto 21 pudo recuperarse relanzando manualmente `_run_project_media_preparation_job(...)`, quedando en `ready` con `Video listo y audio separado. Falta transcribir.`.
- 2026-04-03: Se corrigió una confusión backend entre `preparación inicial del proyecto` y `transcripción atascada`. El endpoint [backend/apps/editor/views.py](backend/apps/editor/views.py) reutilizaba el mismo guard de `processing stale` tanto para la preparación del proyecto nuevo como para la transcripción, y cualquier `processing` viejo terminaba marcado con el mensaje `La transcripcion anterior quedo atascada...` aunque el proyecto ni siquiera hubiera llegado a crear transcript, palabras ni logs API. Ahora el backend clasifica el contexto real del `processing`: si lo que se quedó viejo fue la preparación inicial (`metadata/audio/preview`), responde con `La preparacion inicial del proyecto quedo atascada. Puedes borrarlo o volver a crearlo.`; solo las corridas de transcripción reales conservan el mensaje de retranscripción atascada. Además `POST /transcribe-audio` ya no intenta relanzar una transcripción encima de un proyecto cuyo atasco era de preparación inicial.
- 2026-04-03: Se corrigió otra causa real de proyectos que `no se dejan borrar` en Windows. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `delete_project_with_assets(...)` borraba primero la fila del proyecto pero luego intentaba eliminar de inmediato los archivos físicos dentro de la misma transacción atómica; si el navegador, ffmpeg u otro proceso seguía sujetando el MP4, Windows devolvía `PermissionError [WinError 32]` y eso revertía también el `project.delete()`, haciendo que la card reapareciera como si el botón `Borrar` no hubiera hecho nada. Ahora la baja en DB se confirma primero y la limpieza de archivos/directorios pasa a `transaction.on_commit(...)` con borrado tolerante (`best effort`), de modo que el proyecto desaparece del listado aunque el archivo quede bloqueado temporalmente y se limpie después como huérfano.
- 2026-04-03: Se corrigió la causa backend del error `'Project' object has no attribute 'video'` al crear proyectos nuevos con procesamiento diferido. El worker `_run_project_media_preparation_job(...)` en [backend/apps/editor/views.py](backend/apps/editor/views.py) había quedado desalineado respecto de `ProjectCreateSerializer.create()`: intentaba leer `project.video`, pasar el `Project` completo a `ensure_extracted_audio_track(...)` / `ensure_seekable_preview_video(...)` y guardar metadata en campos que en realidad pertenecen a `VideoAsset`. Ahora el job resuelve el video primario real del proyecto con `_get_project_primary_video(...)`, persiste la metadata sobre ese `VideoAsset` y ejecuta la preparación de audio/preview con el tipo correcto, evitando que el pipeline se detenga antes de lanzar la transcripción.
- 2026-04-03: Se rearmó el arranque de proyectos nuevos para que el modal de proceso por fin muestre trabajo real del backend también durante `POST /projects`. En [backend/apps/editor/serializers.py](backend/apps/editor/serializers.py) `ProjectCreateSerializer.create()` ahora soporta una vía diferida: para uploads de video sin SRT crea rápido `Project` + `VideoAsset` + secuencia base y devuelve enseguida el proyecto en estado `processing`, sin bloquear la respuesta en `probe_video_metadata(...)`, extracción de audio ni preview seekable. En [backend/apps/editor/views.py](backend/apps/editor/views.py) `ProjectListCreateView.create()` lanza luego un worker `_run_project_media_preparation_job(...)` que va publicando etapas reales (`Analizando metadata del video...`, `Extrayendo audio...`, `Generando preview seekable...`) sobre `processing_progress` y `processing_message`. Del lado frontend, [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) ahora espera primero esa preparación con `pollProjectPreparation(projectId)` antes de disparar la transcripción, así que la consola del modal deja de quedarse muda en la ventana donde antes el backend estaba trabajando dentro de la request inicial.
- 2026-04-03: Se cerró el saneamiento del borrado de proyectos y de temporales backend. En [backend/apps/editor/services.py](backend/apps/editor/services.py) `delete_project_with_assets(...)` ahora dispara además una pasada global de `cleanup_orphaned_media_storage()` para podar archivos y directorios huérfanos en `media/videos`, `media/videos/preview`, `media/audio`, `media/timeline` y `media/exports` que ya no tengan respaldo en la DB. En la misma ronda `_transcribe_with_openai_whisper(...)` quedó envuelto en `try/finally` para borrar siempre el directorio temporal de chunks que crea `_ensure_openai_audio_input(...)` al partir audios grandes para OpenAI. Tras ejecutar una limpieza real sobre el workspace actual, `audio/` y `timeline/` quedaron reducidos a los proyectos vivos (`10`, `12`, `14`) y `exports/` quedó sin residuos antiguos.
- 2026-04-03: Se revisó la configuración activa de transcripción antes de una nueva retranscripción y el workspace estaba todavía apuntando a `openai-whisper`, pese a que los proyectos problemáticos mostraban justamente síntomas de salida ASR corrupta en esa ruta (`prompt bleed`, repeticiones absurdas y chunks con contenido numérico). Se dejó el `WorkspaceSettings` activo en `stable-ts` + `large-v3` + `device=auto` para que la próxima corrida use la vía local más fiable mientras se termina de sanear el flujo de OpenAI.
- 2026-04-03: Se cerró una fuente adicional de confusión al retranscribir proyectos: [backend/apps/editor/services.py](backend/apps/editor/services.py) ahora elimina primero las filas viejas de `OpenAITranscriptionLog` con `request_kind="transcription"` antes de persistir la nueva corrida. Con esto el trace del proyecto deja de mezclar chunks de transcripciones anteriores con la actual, algo que en casos como el proyecto 10 ya estaba produciendo varias filas repetidas por `request_index` y dificultaba distinguir si el problema real era de asset duplicado o de transcript corrupto.
- 2026-04-03: Se añadió una calibración manual por proyecto para transcripts desfasados en el shell de `Projects`. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el panel `Proyecto activo` ahora permite aplicar un offset local en segundos a los `start_ms/end_ms` del transcript del proyecto abierto, con persistencia por `projectId` en `localStorage`. Ese ajuste mueve juntos el highlight, la navegación por párrafos, los `IN/OUT` y la selección fuente de la slide 2, lo que permite compensar proyectos viejos ya corridos sin esperar a retranscribirlos y sirve también como diagnóstico rápido: si al aplicar, por ejemplo, `-240s` el texto cae en sync, el problema es de timestamps persistidos y no del player.
- 2026-04-03: Se detectó y corrigió una causa backend de desfase fijo en transcripciones largas con `openai-whisper`. En [backend/apps/editor/services.py](backend/apps/editor/services.py) los chunks posteriores se estaban reubicando usando el `end_ms` reconocido del chunk anterior; eso puede derivar varios segundos cuando OpenAI arranca o termina el chunk con silencios recortados o alucinados. Ahora cada chunk conserva el timeline real del audio usando la duración efectiva del chunk como base de offset, evitando que las palabras de chunks siguientes queden progresivamente adelantadas o atrasadas respecto al video. Este cambio corrige corridas nuevas; los proyectos ya transcritos con offsets heredados necesitan retranscripción para regenerar `start_ms/end_ms` consistentes.
- 2026-04-03: Se endureció la lógica de palabra/párrafo activo en el transcript general del shell de `Projects` para corregir un desfase visible frente al audio. En [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js) la palabra activa ya no cae por fallback en la `más cercana` aunque esté a segundos de distancia: ahora solo se acepta un snap corto alrededor del tiempo real del playhead, evitando que el texto quede varios segundos atrasado o que haga pequeños regresos periódicos en fronteras de palabras. En la misma ronda también se aceleró el auto-follow vertical del transcript, reduciendo la amortiguación que hacía que el scroll visual siguiera al audio con demasiado retraso.
- 2026-04-03: Se corrigió un bucle de sincronización en el preview compartido del shell de `Projects` que hacía que la reproducción pareciera querer retroceder al avanzar por palabras. En [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx) el `<video>` ya no vuelve a aplicarse `currentTime` cuando el `currentTimeMs` entrante es solo un eco del propio playback reportado por el componente, y además se eliminó el `onTimeUpdate` nativo redundante que duplicaba emisiones hacia React. Con eso el preview deja de perseguir tiempos levemente atrasados re-render tras re-render y la reproducción en slide 2 debería sentirse continua en vez de “saltando hacia atrás”.
- 2026-04-03: Se compactó visualmente el modal de procesamiento de transcripción para alinearlo con el lenguaje denso del slide 2. En [frontend/src/styles/app.css](frontend/src/styles/app.css) el modal ahora usa radios más rectos, menos padding, una cabecera más baja, un bloque de resumen de etapa mucho más ceñido y una distribución vertical que reserva más espacio real para `Consola de proceso` y `Texto parcial recibido`. El objetivo fue corregir la sensación de que la tarjeta superior “se sale de caja” o consume demasiada altura sin dejar ver bien la información útil de abajo.
- 2026-04-03: Se amplió la telemetría del modal de procesamiento de proyectos nuevos para que deje de sentirse ciego mientras corre la subida/transcripción. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) la creación del proyecto ya no usa `fetch` simple: ahora sube el video con `XMLHttpRequest` para poder mostrar progreso real de upload, y la consola del modal actualiza en vivo entradas fijas para `POST /projects`, `POST /transcribe-audio`, `GET /transcription-status` y `GET /openai-trace`, incluyendo payload enviado, respuesta recibida y preview parcial cuando exista. Además, durante la sesión local del modal ya también se muestran las requests OpenAI detectadas por la traza, en vez de esconderlas hasta que el proyecto entre por completo en el contexto global.
- 2026-04-03: Se corrigió un bug concreto del borrado en `Project List`. El flujo de `deleteProjects(...)` en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) seguía llamando `removeDraft(...)` después del `DELETE`, pero ese helper no estaba importado, así que el borrado podía romperse en runtime justo después de responder el backend y dejar la card reapareciendo o el estado local inconsistente. En la misma ronda el reload del listado pasó a pedir `/api/editor/projects/` con `cache: "no-store"` y el borrado limpia también snapshot de restore, sugeridas locales y sesiones locales de split review del proyecto, para que al recrearlo se arranque de verdad desde cero.
- 2026-04-03: Se corrigió una lectura engañosa del panel de `Status` para proyectos recién creados. Había casos donde el proyecto quedaba en backend como `ready` con `5%`, mensaje `Video listo y audio separado. Falta transcribir.`, `0 logs API` y `0 transcripts`; aun así el frontend mostraba las etapas como si toda la transcripción hubiese terminado. En [backend/apps/editor/views.py](backend/apps/editor/views.py) el payload de etapas ahora distingue explícitamente el estado `Pendiente de transcribir` cuando no existe transcript real, y en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el panel deja claro que no hay preview ni payloads API porque la corrida no se materializó. Además, la creación de proyecto ya no marca éxito si `launchTranscription(...)` termina sin segmentos ni palabras persistidas.
- 2026-04-03: Se aisló mejor el modal de procesamiento de proyectos nuevos para evitar que reutilice estado del proyecto anterior mientras todavía no cargó el `bootstrap` correcto. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el polling de transcripción ya no escribe `processing_*` sobre un `bootstrap` perteneciente a otro proyecto, y el modal permanece en modo de sesión local hasta que el `bootstrap.project.id` realmente coincide con el `targetProjectId` de la corrida nueva. Con esto deja de mezclarse telemetría vieja durante la ventana en la que el shell ya cambió de foco pero los datos reales del proyecto nuevo todavía no terminaron de hidratarse.
- Fixed the real modal bug: the transcription status portal was mounted only in the editor return path, so processing from the library slide never rendered it. The modal node is now shared across both library and editor views.
- Restyled the slide 1 Project List cards to match the denser slide 2 shell language: tighter spacing, flatter radii, smaller preview thumbnails, squarer chips/buttons, and a compact activity block instead of the oversized duplicated meta row.
- Added a one-time local-dev recovery path in the app error boundary for React hook-order Fast Refresh crashes, so the UI reloads cleanly instead of staying stuck in a broken state after hot updates.
- Moved the global transcription status modal to a React portal mounted on `document.body`, avoiding layout clipping/stacking issues that could leave the UI stuck showing only the inline "Procesando..." state even though the modal had been opened in state.
- Added the global transcription status modal as the primary live monitor for new Project List processing runs, so creating a project now opens the modal immediately and streams backend pipeline events instead of only leaving the CTA in a generic "Procesando" state.
- Reused the existing project worker/API trace aggregation inside that modal to show a real-time process console with worker, audio/storage, API upload/response, payload details, and partial text preview while the new project is being transcribed.
- 2026-04-03: Se recalibró la navegación por rueda entre las slides del shell de `Projects` para volver al comportamiento escalonado que se había acordado. En [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx) el gesto ya no necesita 4 pasos: ahora el primer scroll solo enciende visualmente la slide destino, el segundo revela una transición parcial de ~30% y recién el tercero ejecuta el snap completo. En [frontend/src/styles/app.css](frontend/src/styles/app.css) también se reforzó la respuesta visual del primer y segundo gesto con un tono más vivo en el frame de la slide objetivo, para que el usuario perciba mejor que existe navegación vertical por slides antes de que ocurra el salto completo.
- 2026-04-03: Se hizo un ajuste de corrección sobre el cambio anterior del transcript en `Projects`. El primer intento todavía dejaba un problema molesto: con un rango ya armado, un click simple sobre otro bloque podía colapsar la selección y reiniciar el tramo como si solo existiera un nuevo `In`. En [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js) el click simple ahora solo navega/cambia el bloque activo cuando ya existe un rango, sin destruirlo; la extensión del tramo sigue siendo explícita con `Shift + click`. Además se bajó la agresividad del highlight palabra por palabra en [frontend/src/styles/app.css](frontend/src/styles/app.css), para que el rango se lea más como una banda de selección con límites claros y menos como texto completo encendido.
- 2026-04-03: Se afinó otra vez la interacción del shell de `Projects` para quitar interferencias al preparar rangos y recorrer el preview. En [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js) el rango del transcript general ya no se cierra con segundo click normal: ahora el click simple solo cambia el bloque activo y el rango se extiende de forma explícita con `Shift + click`. En [frontend/src/components/LibraryProjectTranscriptPanel.jsx](frontend/src/components/LibraryProjectTranscriptPanel.jsx) además se volvió mucho más visible qué parte exacta del diálogo pertenece al rango seleccionado, con banda interna, palabras marcadas y chips `IN/OUT` dentro del propio bloque. En paralelo, [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx) dejó de mezclar scrub con eventos de tiempo viejos del video: durante drag el preview pausa, silencia emisiones competidoras y reanuda luego, evitando los saltos aleatorios o regresos al inicio que ocurrían al arrastrar la timeline del preview; también se domesticó el slider de volumen para que responda al click directo sin el foco blanco nativo del navegador.
- 2026-04-03: El preview principal del shell de `Projects` dejó de depender de los controles nativos del navegador y ahora usa un transporte propio en [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx): `play/pausa`, `stop`, saltos cortos, volumen persistido y acciones de `Marcar In/Out` con iconos. En paralelo se corrigió la sensación de scrub "glitcheado" del transcript general en [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js): el auto-follow ya no escribe `scrollTop` en seco sobre cada frame, sino que sigue el playhead con amortiguación y se aparta temporalmente cuando detecta scroll o arrastre manual. La sesión del dashboard también guarda y restaura el tiempo del preview de biblioteca, además del tiempo/scroll del timeline del editor, para volver exactamente al punto donde se dejó tras refrescar.
- 2026-04-03: Además del endurecimiento del iterador de stream, se filtró el ruido que seguía saliendo desde el propio `runserver` de Django. El mensaje `- Broken pipe from ...` no venía ya del endpoint del preview sino de `django.core.servers.basehttp`, que lo loguea explícitamente por `django.server` cuando el navegador aborta una petición `Range`. En [backend/config/settings.py](backend/config/settings.py) se añadió una configuración de logging dedicada para `django.server`, y [backend/config/logging_filters.py](backend/config/logging_filters.py) filtra solo esa línea concreta sin ocultar los accesos HTTP normales del backend.
- 2026-04-03: El stream HTTP del preview de video en backend se endureció frente a scrub/seek agresivo desde timeline o preview. Al mover el playhead, el navegador abre una nueva petición `Range` y cancela la anterior; en desarrollo eso estaba ensuciando la terminal con `Broken pipe`. El iterador de [backend/apps/editor/views.py](backend/apps/editor/views.py) ahora trata esas cancelaciones (`BrokenPipeError`, `ConnectionResetError`, `GeneratorExit`) como cortes normales del cliente y además cierra siempre el archivo en `finally`.
- 2026-04-03: El control de rango no quedó limitado al preview lateral del `Script principal`. El `Preview` grande del shell de `Projects`, que usa [frontend/src/components/SharedPreviewSurface.jsx](frontend/src/components/SharedPreviewSurface.jsx), ahora también expone una timeline visible para scrub, overlay del rango actual, playhead, y botones `Marcar In/Out` + `Ir In/Out`. De esa forma el usuario puede fijar el tramo directamente en el panel central de preview que está viendo mientras recorre el transcript general.
- 2026-04-03: El `Preview selección` del `Script principal` dejó de ser solo una vista pasiva del tramo ya elegido y pasó a servir también para construir y ajustar el rango fuente. Ahora suma una línea de tiempo propia para scrub, marcas explícitas `Marcar In` / `Marcar Out`, atajos de teclado `I` y `O`, y esas marcas recalculan la selección real del transcript en vez de guardar otro estado paralelo. Como la selección fuente ya forma parte del draft persistido, el rango vuelve correctamente tras refresh en este navegador y sigue siendo el mismo insumo para `Crear secuencia` y para enviar tramos al flujo de sugeridas.
- 2026-04-03: El transcript general del shell de `Projects` ganó una mecánica más intuitiva para seleccionar rangos por bloques. Un primer click sobre un párrafo fija el ancla; un segundo click sobre otro párrafo cierra el rango hacia delante o hacia atrás; y `Shift + flecha arriba/abajo` ahora expande o contrae la selección por grupos de palabras completos siguiendo la dirección. Con esto ya no hace falta bajar siempre al nivel palabra a palabra para preparar secuencias manuales o definir el tramo que después se usará como base de trabajo.
- 2026-04-03: Se corrigió el runtime reciente del shell de `Projects` que aparecía al introducir la timeline vertical con pausas explícitas en el transcript de la slide 2. La causa era un desacople entre el hook [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js), que ya devolvía `libraryProjectGapItems` y `libraryProjectGapHeights`, y el render en [frontend/src/pages/DashboardPage.jsx](frontend/src/pages/DashboardPage.jsx), que no estaba pasando esas props a [frontend/src/components/LibraryProjectTranscriptPanel.jsx](frontend/src/components/LibraryProjectTranscriptPanel.jsx). Además de cablear esos datos, el panel quedó endurecido con defaults seguros para que una omisión similar no vuelva a tumbar todo el runtime con un `undefined.find(...)`.
- 2026-04-03: Se rehízo la librería de proyectos del shell para que deje de sentirse como una lista plana y desperdiciar alto vertical cuando hay pocos items. `Project List` ahora usa una grilla responsive de cards más densas con preview de video en la propia tarjeta, CTA explícito `Abrir`, métricas visibles (duración, resolución, cantidad de secuencias y hora), pista textual de interacción (`click selecciona`, `Abrir`/doble click entra) y una jerarquía visual más fuerte entre `seleccionado` y `abierto en shell`. Con esto el doble click sigue existiendo como atajo, pero ya no queda escondido como única forma de entender cómo entrar al proyecto.
- 2026-04-02: El transcript de la slide 2 del shell de `Projects` dejó de comportarse como una lista que solo se recoloca entre bloques y pasó a una timeline vertical continua. Entre párrafos ahora se renderizan huecos visuales explícitos de pausa con un tono celeste configurable por theme, y su altura en píxeles se calcula según la duración del silencio usando una velocidad promedio local derivada de los bloques vecinos. El seguimiento del playhead ya no interpola solo entre texto y texto: ahora navega por una pista temporal compuesta por párrafos + gaps, así que durante silencios también sigue avanzando verticalmente en lugar de quedarse congelado.
- 2026-04-02: Se volvió a afinar el seguimiento del transcript en la slide 2 del shell de `Projects` para acercarlo más a un comportamiento de lectura continua. En vez de centrar solamente el párrafo activo, el seguimiento ahora toma como ancla la palabra realmente activa y anima el scroll hacia esa posición, lo que hace que el movimiento entre grupos de texto se sienta más permanente y menos como un salto por bloques. En paralelo, el `playhead` visual dejó de usar halo/fondo decorativo: la palabra en turno ahora se apoya principalmente en peso tipográfico alto y color del theme (`playhead text`) para que el look sea más limpio y personalizable.
- 2026-04-02: Se afinó otra vez la lectura viva del transcript en la slide 2 del shell de `Projects`. El seguimiento del párrafo activo ya no reposiciona en seco: al avanzar la reproducción entre bloques, el scroll del transcript ahora anima el desplazamiento para acompañar la lectura en vez de saltar bruscamente. En paralelo se separó mejor la jerarquía visual entre `seleccionado` y `en reproducción`: la selección persistente del usuario pasó a una presencia más fría y sutil, mientras la línea realmente activa conserva el ámbar del tema y la palabra en reproducción ganó peso tipográfico, fondo/halo y color `playhead` parametrizado para que después se pueda personalizar sin volver a tocar la lógica.
- 2026-04-02: Se corrigió otro runtime stop del shell de `Projects` causado por una dependencia en zona temporal muerta (`libraryProjectActiveWord`) dentro de `DashboardPage.jsx`. Para que no vuelva a repetirse por simples cambios de orden, la sincronización del transcript/preview/navegación del workspace del proyecto salió a un hook dedicado [frontend/src/pages/useLibraryProjectShell.js](frontend/src/pages/useLibraryProjectShell.js) y el render del transcript general de la slide 2 pasó a [frontend/src/components/LibraryProjectTranscriptPanel.jsx](frontend/src/components/LibraryProjectTranscriptPanel.jsx). Con esto `DashboardPage.jsx` deja de mezclar en la misma zona estado, memos, efectos de teclado y JSX del transcript del shell, y la navegación del workspace queda encapsulada en subarchivos más pequeños y mantenibles.
- 2026-04-02: Se ajustó la navegación fina del transcript en la slide 2 del shell de `Projects`. El preview/transcript del shell ahora intercepta `flechas` para avanzar o retroceder palabra por palabra sobre el playhead activo, manteniendo sincronizados preview, palabra resaltada y centrado del párrafo en lectura. En la misma corrección se eliminaron los focos blancos por defecto del navegador sobre tarjetas del transcript y sobre la superficie compartida de preview, reemplazándolos por un foco ámbar consistente con el highlight activo para que la palabra en curso siga siendo el estado visual dominante incluso al usar `Espacio` o teclado de navegación.
- 2026-04-02: Se corrigió la causa raíz del preview seekable cruzado que seguía mostrando otro video aunque el proyecto, transcript y `video_id` ya fueran correctos. El backend reutilizaba cualquier `video_{id}_seekable.mp4` existente sin validar si todavía correspondía al archivo fuente actual; en la práctica, si el source se reemplazaba o el asset cambiaba después de haber generado ese preview, el sistema seguía sirviendo un artifact viejo. Ahora `ensure_seekable_preview_video(...)` invalida ese cache cuando el source es más nuevo que el preview o cuando la duración del preview difiere de la duración esperada del video, forzando la regeneración del MP4 seekable correcto.
- 2026-04-02: Se modularizó una parte del shell de `DashboardPage` para bajar fragilidad en runtime y dejar de cargar tanta lógica de layout dentro del mismo archivo gigante. Los helpers del shell de biblioteca/proyecto (`ids` de paneles, labels, layouts, utilidades de párrafo y formateo de telemetría) salieron a [frontend/src/pages/dashboardShellHelpers.js](frontend/src/pages/dashboardShellHelpers.js). En la misma ronda se corrigió una causa concreta de página colgada tras HMR: la slide 1 estaba recreando su array de paneles dentro del render y eso contaminaba dependencias de `useMemo/useEffect` que normalizan el dock, abriendo la puerta a renders en cascada. Ahora esos ids viven como constantes estables y el layout visible de la slide 1 se recompone desde helpers compartidos.
- 2026-04-02: El menú global `Panels` dejó de ser fijo y ahora cambia según la slide activa del shell de `Projects`: en la slide 1 expone `Sube un video` y `Project List`, y en la slide 2 publica únicamente los módulos del workspace del proyecto. Ambas slides ya guardan visibilidad propia, así que se pueden cerrar todos los paneles de una slide, dejarla vacía y recuperar luego solo los de ese contexto desde el header sin mezclar catálogos entre slides. En paralelo se corrigió la causa real del preview equivocado (`Geely` en lugar de `TVS`): el backend ya no toma el primer video creado del proyecto para bootstrap/stream/timeline, sino el video ligado al transcript activo y, si no existe, cae al video más reciente. En la misma ronda se comprimió otra vez la geometría del shell: menos padding exterior, secciones snap más apretadas, divisores del dock bastante más finos y radios globales todavía más cuadrados.
- 2026-04-02: La slide 2 del shell de `Projects` ahora suma un panel real de `Status` dentro del dock. En vez de depender solo del modal de transcripción, el workspace del proyecto muestra ahí el estado del pipeline, su progreso, etapas activas, consola compacta por procedencia (`worker local`, `audio/storage`, `API upload`, `API response`) y el preview parcial del transcript. El panel reutiliza la telemetría que ya existía (`processing_message`, `processing_stage`, `processing_preview_text`) y además incorpora la traza OpenAI por request para exponer chunk, tamaño, duración, velocidad aproximada, payload enviado, payload recibido, request id y preview textual de la respuesta. En la misma ronda se corrigió un bug de arrastre de estado en el preview reutilizable: al cambiar de proyecto, la slide 2 resetea el punto de lectura y fuerza remount por `videoUrl`, evitando que el `<video>` conserve frames o duración del proyecto anterior.
- 2026-04-02: La slide 2 del shell de `Projects` dio otro salto hacia una UI de herramienta más densa y tipo escritorio técnico. Se compactaron todavía más paddings, gaps, radios y chrome general; el layout persistido de esta slide pasó a una versión nueva con transcript a la izquierda, preview reutilizable y tabs inferiores para `Proyecto activo` / `Secuencias + IA`; el resize del dock dejó de depender de porcentajes rígidos y ahora usa ratios `flex` con divisores mucho más visibles. En paralelo, el transcript general dejó de ser pasivo: click sobre una tarjeta selecciona el tramo, lo resalta, pinta el rail con el color activo del tema y sincroniza el preview. El preview de slide 2 ya no es un `<video>` aislado, sino una superficie compartida con chips de aspecto reutilizables y metadatos compactos, preparada para que otros previews del programa converjan hacia la misma base. También se añadió una primera capa de transición por rueda entre slides: el primer gesto insinúa visualmente el destino, los siguientes avanzan parcialmente y recién al acumular intención se fuerza el snap completo.
- 2026-04-02: La slide 2 del nuevo shell de `Projects` ya quedó equipada como workspace manejable de paneles. Se añadió un panel real de `Preview` junto a `Transcript general` y `Proyecto activo`, cada panel ahora expone cierre directo desde su propia cabecera y la topbar global ganó un botón `Panels` con menú desplegable estilo herramienta de escritorio: lista los paneles disponibles de esta slide con checkboxes para activarlos o desactivarlos al vuelo. La visibilidad de esos paneles se persiste localmente y, si se cierran todos, la slide queda en estado vacío recuperable desde ese mismo menú del header.
- 2026-04-02: Arrancó la estructura de shell con `sticky header` y secciones snap para la página principal del proyecto. El `App` ahora expone un `app-content` fijo debajo de la barra superior, la cabecera queda anclada arriba y `Projects` pasó a renderizarse como un scroll vertical por slides dentro de un `snap shell`. La primera slide ya contiene el workspace modular de `Sube un video + Project List`; la segunda quedó sembrada como base del próximo tramo (`Transcript general`). La regla adoptada para convivir con módulos internos con scroll es esta: el scroll principal pertenece al contenedor de secciones, pero cada módulo mantiene su propio `overflow`; mientras el módulo todavía puede desplazarse, el gesto se queda local, y recién al agotarse ese scroll el usuario transita naturalmente a la siguiente slide con snap.
- 2026-04-01: Se cambió también la línea cromática del shell/dock para acercarla al lenguaje del editor en vez del azul genérico anterior. La base visual ahora usa más acero/gris como estructura, ámbar para foco/acción y verde para estados válidos o selección estable, con una lectura más de herramienta industrial tipo modelador 3D clásico (`LightWave` / UI de estudio técnico) que de dashboard web moderno. El cambio tocó topbar, tabs del dock, botones primarios, cards de proyecto, drop zones y superficies de biblioteca para que todo el chrome se sienta parte de la misma familia visual del editor.
- 2026-04-01: Se endureció todavía más la regla visual de botones compactos. `Cambiar video` y `Procesar` dentro del library dock seguían viéndose altos y con centrado inconsistente porque el sistema base no trataba todos los botones como `inline-flex` compactos. Ahora los botones comparten alineación vertical real, alturas más bajas (`24-30px` según variante), menos padding, tipografía más pequeña y radios casi rectos, con una lectura más cercana al lenguaje `cyberdeck / cassette futurism` pedido: controles tensos, funcionales y con menos chrome blando.
- 2026-04-01: Se comprimió otra vez la densidad visual del shell siguiendo la regla de que el pixel vertical es caro. El library dock y la base compartida del frontend bajaron tipografía, paddings, alturas mínimas y radios para acercarse a una estética mucho más compacta y casi rectangular: topbar más baja, tabs y panel headers más finos, botones chicos con alturas de `28-32px`, `panel-body` más corto y radios reducidos. En la biblioteca también se recortó copy redundante (`Flujo compacto...`, hints secundarios, subtítulos de módulos y texto explicativo repetido en `Video elegido`), y el `textarea` de descripción pasó a un footprint menor. La intención fue que incluso en módulos estrechos el layout siga siendo limpio, legible y con mucho menos desperdicio vertical.
- 2026-04-01: Se ajustó el library dock para respetar reglas modulares reales de densidad y reflujo. El intake de `Sube un video` todavía dependía demasiado del viewport global y, cuando el módulo se estrechaba dentro de un split, seguía intentando sostener una composición horizontal con huecos inútiles y solapes visuales. Ahora la biblioteca usa `container-type: inline-size` a nivel de módulo, así que el reacomodo ocurre por ancho real del panel: el intake deja de reservar una tercera columna vacía, los subpaneles internos (`form` y `video elegido`) pasan a apilarse verticalmente cuando el contenedor se hace angosto y las acciones internas también se vuelven de una sola columna en tamaños extremos. En la misma ronda se eliminó del header el texto accesorio del lado derecho (`Agrega un video o abre un proyecto existente`) y se comprimieron paddings/alturas para acercar el layout a la regla de que cada pixel vertical debe justificar su existencia.
- 2026-04-01: Se ajustó otra vez la UX de tabs del docking para priorizar estabilidad real del drag. El intento de colapsar visualmente la pestaña fuente mientras se arrastraba terminó interfiriendo con el drag nativo y, en la práctica, podía dejar al usuario sin propuestas visibles o sin reacción al seguir moviendo el mouse. La solución final fue más conservadora: el tab fuente se mantiene intacto durante el drag, el stack calcula el `drop target` usando la pestaña restante del grupo y la cabecera de tabs deja de renderizar el texto/estado accesorio del lado derecho. Con eso la tira superior queda más limpia, más compacta y el comportamiento de arrastrar-volver-a-soltar deja de depender de mutaciones agresivas del propio nodo fuente.
- 2026-04-01: Se corrigió un bug más preciso del docking durante la propuesta de ubicación. Las zonas visuales de `arriba`, `abajo`, `izquierda`, `derecha` y `tab` sí aparecían, pero la activación dependía de `dragenter/dragover` sobre botones internos de cada celda; en la práctica, al cambiar el layout en pleno drag, había casos donde el hover no terminaba de fijar ninguna zona activa y el overlay se quedaba "muerto". Ahora el hit-testing del drop vive en el contenedor completo del módulo: según la posición real del cursor dentro del rectángulo se calcula la zona activa y las celdas quedan como overlay puramente visual. Con eso la sugerencia de ubicación deja de depender de focos o eventos frágiles de cada celda individual.
- 2026-04-01: Se corrigió otra capa de la redock de tabs dentro del nuevo docking. No bastaba con cambiar el `target` interno del drop: el primer intento quitaba del layout la pestaña arrastrada durante el drag, y con drag nativo HTML5 eso dejaba el hover inestable o directamente congelado. El ajuste correcto fue mantener la pestaña fuente montada en el DOM pero colapsada visualmente dentro de su tira, mientras el stack usa la pestaña restante como referencia activa para volver a mostrarse como contenedor divisible. Con eso el módulo origen sigue sintiéndose “liberado” para redock, pero sin romper la continuidad de `dragover`. En la misma ronda se dejó la cabecera del stack como una sola tira superior mucho más compacta, para que los tabs ocupen de verdad el borde alto del módulo y no desperdicien altura vertical.
- 2026-04-01: Arrancó la migración del frontend hacia un sistema real de módulos acoplables. Se añadió un motor reusable de docking en frontend con cinco zonas de drop (`arriba`, `abajo`, `izquierda`, `derecha`, `tab`), divisores persistentes, tabs y layout guardado por vista en `localStorage`. El primer rollout ya cubre las tres vistas superiores de la app: `Projects` (biblioteca), `Status` y `Settings`, todas con UI más densa y controles compactos. El editor principal todavía conserva parte de su layout histórico interno, pero la base técnica para seguir migrándolo ya quedó integrada.
- 2026-04-01: Se rehízo el flujo de `Nuevo proyecto` en biblioteca. Antes `Seleccionar video` disparaba de inmediato toda la creación/transcripción y la tarjeta superior quedaba visualmente aplastada y poco clara. Ahora el alta pasa a ser un flujo de dos pasos: `Seleccionar video` solo carga el archivo, muestra preview local del video elegido y habilita un botón explícito `Procesar`; recién entonces se crea el proyecto y se reutiliza el modal de progreso para ese procesamiento inicial, con copy orientado a `Procesando proyecto nuevo` en vez de `Retranscribiendo proyecto`.
- 2026-04-01: Se endureció otra vez el borrado de proyectos bloqueados por SQLite. La base seguía corriendo en `journal_mode=delete`, así que lecturas concurrentes del dashboard podían bloquear escrituras destructivas como `DELETE project`. Ahora las conexiones SQLite se levantan en `WAL`, con `busy_timeout` más alto y `synchronous=NORMAL`; además, el `destroy` de proyecto cierra todas las conexiones Django, fuerza GC y reintenta más veces con backoff antes de devolver el error de `database is locked`. La intención es que borrar desde la biblioteca deje de fallar cuando el propio backend o el frontend mantienen lecturas vivas sobre la base.
- 2026-04-01: Se protegió `Retranscribir` en proyectos ya editados. Después de varias rondas quedó claro que volver a generar el transcript base sobre un proyecto con secuencias creadas o sugeridas puede invalidar toda la edición, incluso con heurísticas de remapeo. Mientras no exista versionado real de transcript/edición, el frontend ahora bloquea esa acción cuando el proyecto ya tiene trabajo editorial y explica explícitamente que debe hacerse solo sobre un proyecto duplicado o limpio. La prioridad pasó de “intentar salvarlo todo después” a “no permitir una operación destructiva sobre un proyecto ya trabajado”.
- 2026-04-01: Se cambió la estrategia de fondo para que `Retranscribir` deje de romper secuencias. Hasta ahora el draft del editor seguía teniendo como verdad principal listas de `wordIds`, y cualquier reasociación posterior intentaba remapear esas palabras sobre el transcript nuevo. Eso resultó frágil porque la retranscripción puede regenerar ids, mover cortes y hasta desplazar el mismo contenido dentro del eje temporal. La solución adoptada pasa a tratar cada secuencia como una lista de clips estables del video (`source_in_ms/source_out_ms`) serializados dentro del draft; al guardar, esos clips se reconstruyen desde el estado real del editor, y al rehidratar después de retranscribir se usan primero esos rangos de fuente para volver a levantar la secuencia sobre el transcript nuevo. Solo si faltaran esos clips se cae a heurísticas de remapeo por texto/tiempo. Con esto el anclaje principal vuelve a ser el video, que sí permanece estable entre transcripciones.
- 2026-04-01: Se compactó todavía más el modo editor-only y se hizo accesible desde el icono superior izquierdo del shell. Al pulsar `VE`, el dashboard ahora alterna entre la vista normal y una versión mucho más baja del header que oculta el chrome del proyecto para dejar solo el editor visible; al volver a pulsarlo restaura el layout habitual con sus módulos y botones. Ese estado sigue la misma persistencia del layout, así que si se deja el proyecto en modo compacto vuelve igual al recargar. En la misma ronda, `Retranscripción en vivo` dejó de vivir arriba en la cabecera y pasó a renderizarse dentro de `Script principal`, también con su estado minimizado persistido para no desperdiciar altura vertical.
- 2026-04-01: Se reforzó otra vez la reparación post-`Retranscribir` al confirmar un corrimiento real de contenido dentro del eje temporal. En el proyecto 10 se comprobó que el texto correcto de una secuencia empezaba cerca de `2193580 ms` (`"Bueno mira este también..."`), mientras la secuencia rota seguía anclada en `2210700 ms`, donde ya arrancaba `"preguntar al experto..."`. Eso demostró que no bastaba con aproximar por tiempo: el contenido hablado se había desplazado dentro del transcript. La recuperación ahora intenta primero encontrar anclas textuales exactas de cabecera y cola del bloque viejo dentro del transcript nuevo y solo después remapea las palabras internas y sus cortes finos dentro de esa ventana textual encontrada.
- 2026-04-01: Se corrigió una causa más precisa de desincronización después de `Retranscribir`. El frontend ya remapeaba `wordIds` viejas a `wordIds` nuevas, pero los cortes finos (`customStartPoints`, `customEndPoints`, `customSplitPoints`) conservaban los `sourceMs` absolutos del transcript anterior. El resultado era una secuencia cuyo texto ya pertenecía al transcript nuevo pero cuyo clip seguía arrancando o cortando en tiempos del transcript viejo, provocando exactamente el síntoma de “el video no inicia donde empieza el texto”. Ahora, al remapear una secuencia desde el backup previo a retranscribir, también se recalculan esos `sourceMs` respecto a la nueva palabra equivalente para que la ancla temporal viaje junto con el texto.
- 2026-04-01: La recuperación tras `Retranscribir` dejó de depender únicamente de ids exactas y pasó a usar aproximación temporal contra el transcript nuevo. Cuando existe el backup local previo a la retranscripción, el frontend ahora toma las palabras viejas del draft, busca para cada una la palabra nueva más cercana por tiempo (con una pequeña ayuda del texto cuando coincide), reconstruye la secuencia sobre ese tramo remapeado y rehace también `breaks`, puntos de corte, silencios colapsados, selección fuente e historial. Además, si al abrir un proyecto el draft actual ya quedó roto por ids caducadas, se intenta autorrepararlo desde ese backup previo y se guarda otra vez con las ids nuevas para no seguir reabriendo el estado corrupto.
- 2026-04-01: Se afinó la recuperación de sugeridas guardadas después de `Retranscribir`. El primer fallback por `start_ms/end_ms` resolvía el caso en que los `word_ids` viejos ya no existían, pero en entrevistas como `Entrevista a Andrea y su Jimny` reabría un tramo demasiado ancho y daba la sensación de texto desincronizado. Ahora, cuando el frontend tiene que reconstruir una sugerida por rango temporal, primero toma las palabras actuales de ese rango y luego recorta el tramo usando keywords derivadas del contexto guardado de la propia sugerida (`sequence_name`, `main_title`, `hook_text`, `conversation_summary`, `coverage_explanation`). Con eso la reasociación deja de limitarse a “algo dentro de ese tiempo” y pasa a buscar la subsección que mejor encaja con el contenido editorial original.
- 2026-04-01: Se corrigió otra desasociación después de `Retranscribir`. Las sugeridas guardadas seguían persistiendo `word_ids` del transcript anterior; cuando la retranscripción generaba un transcript nuevo, esas ids ya no existían y abrir casos como `Entrevista a Andrea y su Jimny` devolvía un rango vacío aunque el proyecto sí tuviera texto. El frontend ahora intenta reconstruir la sugerida por `start_ms/end_ms` sobre las palabras actuales del proyecto cuando los `word_ids` viejos ya no resuelven, así que reabrir una sugerida previa vuelve a mostrar texto aunque la numeración interna de palabras haya cambiado entre transcripciones.
- 2026-04-01: Se detectó la causa raíz de un caso de retranscripción absurda con `openai-whisper`. El backend estaba enviando un `prompt` instructivo largo (`"El audio pertenece a un editor de video transcript-first..."`) y en el proyecto 10 OpenAI terminó devolviendo justamente ese texto repetido como si fuera parte del audio. Se corrigió el flujo para que `openai-whisper` ya no reciba instrucciones editoriales en el `prompt`: ahora, como mucho, se le pasa una cola corta del chunk anterior para mantener continuidad entre cortes. Además `Retranscribir` y `Restaurar backup` salieron del header de sugeridas y se movieron junto a `Script principal`, que es el lugar natural desde donde relanzar o revertir el transcript base del proyecto.
- 2026-04-01: Se corrigió una fuga importante del modal de retranscripción. Aunque el backend sí iba acumulando `processing_preview_text` durante la corrida, al marcar `ready` lo vaciaba otra vez y el modal final quedaba sin `Texto parcial recibido`, dando la impresión de que nunca había llegado nada. Ahora ese preview se conserva también al terminar, y el frontend además dejó de encerrar la parte baja del modal sin scroll útil: el cuerpo completo ya se puede recorrer y las secciones internas (`etapas`, `texto parcial`) tienen límites y scroll propios para no quedar ocultas fuera del viewport.
- 2026-04-01: El modal de retranscripción del proyecto dejó de mostrar solo un mensaje genérico y ahora expone etapas reconocibles del pipeline. El backend publica `processing_stage` con la etapa actual y la línea de tiempo (`En cola`, `Preparar audio`, `Transcribir`, `Guardar segmentos`, `Guardar palabras`, `Listo`), y el frontend usa eso para pintar qué ya se completó, qué está corriendo y qué falta. En la misma vista también se calcula `tiempo transcurrido`, `tiempo restante` y `termina aprox.` a partir del progreso real de la corrida actual, para que sea mucho más claro cuánto falta y en qué punto exacto está el proyecto.
- 2026-04-01: La retranscripción del proyecto dejó de depender solo de un `status-pill` fácil de perder. Ahora cualquier operación de transcripción a nivel proyecto abre un modal dedicado que muestra progreso, mensaje vivo del backend y preview parcial del texto según va llegando; mientras el proceso sigue corriendo no se puede cerrar, y al terminar o fallar queda visible con botón `Cerrar` para revisar el resultado antes de salir.
- 2026-04-01: `Retranscribir` ya deja un punto de retorno mucho más fiel dentro del navegador actual. Antes del rerun el frontend ahora guarda no solo el draft, sino también el `bootstrap` y los `wordTimings` vigentes; además se añadió `Restaurar backup` junto a `Retranscribir` para rehidratar proyecto, secuencias, transcript y layout desde ese snapshot local. Si el backup disponible es viejo y no trae transcript/base completos, el botón queda deshabilitado y explica que ese formato anterior no alcanza para reconstruir el proyecto entero. La apertura del proyecto también pasó a respetar una restauración local guardada, así que ese estado restaurado sobrevive al reabrir el proyecto en este mismo navegador.
- 2026-04-01: La biblioteca de proyectos ya no obliga a refrescar manualmente tras borrar. En cuanto el backend confirma el `DELETE`, el frontend saca de inmediato las cards borradas del estado local y deja `loadProjects()` solo como reconciliación posterior. Con eso el listado refleja el cambio en el acto y evita la sensación de que el borrado "no pegó" hasta recargar la página.
- 2026-04-01: El borrado de proyectos dejó de reventar el frontend con el mensaje genérico "El backend devolvió una respuesta inválida". La causa real era un `database is locked` de SQLite durante el `DELETE` del proyecto. Ahora el backend reintenta unas veces antes de rendirse y, si sigue bloqueado, responde JSON claro con error utilizable; además, el frontend ya no colapsa al recibir HTML o cuerpos no JSON y puede mostrar un mensaje accionable.
- 2026-04-01: La biblioteca de proyectos pasó a distinguir selección y apertura. Ahora un click simple sobre una card solo la selecciona para administrar, el doble click la abre, la cabecera del listado muestra cuántos proyectos están elegidos y cada card seleccionada expone `Borrar` junto al estado `ready/error`. Con esto deja de ser necesario abrir por accidente un proyecto solo para poder limpiarlo.
- 2026-04-01: Se investigó el daño de `Retranscribir` sobre el proyecto 10 a nivel de persistencia real. El backend actual ya solo conserva un draft colapsado (`recovered-sequence-10`) y un `EditSequence` con un solo clip, así que el problema no era solo visual. Para no perder más estado en próximas corridas, el frontend ahora guarda un backup local timestamped del draft justo antes de relanzar `Retranscribir`.
- 2026-04-01: Se añadió el utilitario [backend/tools/recover_editor_draft_from_freelist.py](backend/tools/recover_editor_draft_from_freelist.py) para extraer snapshots editoriales desde páginas libres del `db.sqlite3` sin mutar la base por defecto. Con esa vía se recuperó y exportó un snapshot parcial del proyecto 10 en [docs/recovery/project_10_candidate_01.json](docs/recovery/project_10_candidate_01.json), junto con su manifiesto en [docs/recovery/project_10_manifest.json](docs/recovery/project_10_manifest.json). El candidato recuperado contiene 5 secuencias antiguas del flujo de división: `Jimny overland con cama imperial`, `Parte inicial no cubierta`, `Lo próximo que quiere modificar`, `Presentación de Andrea y accesorios del Jimny` y `Cómo le ha ido con el Jimny`.
- 2026-04-01: Cada secuencia persistida del editor ahora conserva también un baseline original serializado para poder volver siempre a su estado de arranque. Las secuencias nuevas nacen con ese snapshot desde su creación, las derivadas desde clips preservan como origen su estructura inicial y las secuencias antiguas que no lo tenían quedan sembradas con el estado cargado actual para fijar un punto de retorno estable hacia adelante. En la UI se añadió `Restaurar original` dentro del editor de secuencia, reutilizando helpers compartidos de hidratación/serialización en vez de lógica duplicada local.
- 2026-03-31: Se endureció la construcción de bloques y párrafos frente a silencios enormes. Cuando una secuencia mantiene gaps muy grandes sin colapsar manualmente, ya no se estira un único clip visible a través de todo ese vacío: el frontend ahora toma esas pausas largas como frontera dura de bloque y también de párrafo. Con eso transcript y timeline dejan de sentirse contradictorios en casos como la secuencia 4, donde antes el texto posterior a un silencio de decenas de segundos seguía apareciendo como parte del mismo clip visual.
- 2026-03-31: Se corrigió la desalineación entre transcript y timeline durante navegación y scrub. Las flechas dentro del transcript ya no solo mueven selección/scroll: ahora también arrastran el playhead y el bloque activo item por item, incluyendo gaps, para que timeline, preview y texto reflejen el mismo punto de lectura. En paralelo, el item activo del transcript dejó de caer por fallback en el final del documento cuando el playhead caía entre rangos sin match exacto, y la aguja de la timeline subió de z-index para quedar visible por encima de thumbnails, handles y demás chrome del timeline.
- 2026-03-31: Se corrigió otra causa de inestabilidad en la navegación del transcript con flechas. El recorrido por palabras y gaps seguía leyendo foco, anchor y selección desde estado React normal, así que al mantener una flecha apretada algunos eventos entraban todavía con valores viejos y la selección podía quedarse parada, avanzar solo una o dos palabras o saltar raro entre items. Ahora el transcript mantiene una ruta única de selección/foco con refs vivas y actualiza estado+refs a la vez, lo que estabiliza `flechas` y `Shift + flechas` también durante repeticiones rápidas.
- 2026-03-31: Se separó de forma explícita el contexto de teclado entre transcript y timeline. Antes bastaba con que quedara una palabra enfocada para que las flechas y `Shift + flechas` se mezclaran con la selección textual aunque el usuario acabara de elegir un clip. Ahora el comportamiento depende de la superficie activa: click en transcript pone el teclado en modo texto; click, drag o preview sobre clip lo devuelve a modo timeline. Con eso `Shift + flechas` ya no debería robarse entre transcript y timeline por restos de foco anteriores.
- 2026-03-31: La selección por teclado dentro del transcript dejó de tratar los silencios detectados como huecos muertos. Ahora palabras y `gap tokens` forman una sola secuencia navegable: las flechas pueden caer sobre el espacio, `Shift + flechas` sombrea también esos gaps dentro del rango y las acciones del editor (`Delete/Backspace`, `Enter`) operan solo sobre las palabras reales contenidas en esa selección mixta, sin romper la sensación mecánica continua del transcript.
- 2026-03-31: Se afinó otra causa del salto molesto en el transcript de secuencia. El click normal sobre palabra ya no mueve el playhead ni fuerza la sensación de que el editor “se escapa” hacia abajo; esa acción pasa a `Alt + click`, mientras que el click simple queda reservado para seleccionar texto. En paralelo, las flechas izquierda/derecha priorizan otra vez el transcript cuando ya hay contexto de selección/foco en palabras, y el `pointerdown` de los botones-palabra deja de ceder al foco-scroll nativo del navegador.
- 2026-03-31: Se rehízo la interacción entre transcript y timeline al elegir un clip. Ahora el clip seleccionado se ve mucho más claro en timeline y transcript, elegirlo desde la timeline centra su arranque dentro del módulo de texto para dejar contexto arriba y abajo, `Shift + flechas` dejó de reordenar bloques y pasó a extender el clip palabra por palabra usando texto inactivo contiguo, y `Ctrl/Cmd + flechas` tomó el reorder de bloques. También se reforzó visualmente la palabra en lectura con un encuadre más evidente para ubicar mejor el punto exacto del clip en el transcript.
- 2026-03-31: Se corrigió una regresión de carga del dashboard que no aparecía en `npm run build` pero sí rompía el runtime en navegador. El `<main>` raíz había quedado contaminado con un `className` copiado desde el botón-palabra del transcript, referenciando `word`, `blockMeta` y otros símbolos fuera de scope; eso disparaba `word is not defined` y dejaba la página en blanco. El render principal volvió a usar `workflow-shell`, así que el dashboard puede montar otra vez.
- 2026-03-31: Se corrigió otro falso negativo de carga del dashboard. El editor fino seguía restaurándose minimizado por defecto (`editorWorkbenchCollapsed`) incluso cuando el proyecto ya tenía secuencia activa, así que al recargar la página podía parecer que “no cargaba” y solo se veía el dock `Abrir transcript, preview y timeline`. Ahora el workbench abre visible por defecto y también se fuerza abierto al restaurar un proyecto con secuencias.
- 2026-03-31: Se afinó la relación timeline/transcript para selección de clips. Elegir un clip ya no conserva la selección textual anterior, el foco del transcript se centra por párrafo alrededor del bloque elegido en vez de quedarse clavado en una palabra suelta y `Shift + flechas` en modo clip pasó a extender el bloque seleccionado palabra a palabra usando texto contiguo disponible, con repetición acelerada al dejar la tecla apretada. El reorder del clip se mantiene en `Ctrl/Cmd + flechas`.
- 2026-03-31: Se afinó el scroll direccional del transcript al crecer clips con `Shift + flechas`. Antes, al extender hacia la izquierda, el efecto genérico del bloque seleccionado podía volver a centrar el transcript en un punto poco útil y dar la sensación de que el scroll “bajaba” en lugar de mostrar contexto previo. Ahora la extensión enfoca la palabra recién absorbida con un sesgo direccional: hacia la izquierda deja más contexto visible arriba, hacia la derecha deja más contexto visible abajo, y el foco general del clip ya no pisa ese comportamiento en cada cambio de borde.
- 2026-03-31: Se corrigió una integración parcial del editor de secuencia dentro del dashboard. El transcript estaba usando botones-palabra que no recibían toda la capa de atajos del editor y, además, cada click movía el playhead con auto-scroll centrado, así que al seleccionar texto era fácil perder la zona visible. Ahora el editor separa mejor selección y playhead: `Shift + flechas` extiende selección palabra a palabra dentro del transcript, `Delete/Backspace` y `Enter` responden desde la propia palabra enfocada sin necesitar un estado previo, y el auto-scroll del playhead deja de pelearse con la selección manual inmediata.
- 2026-03-31: El split studio ya permite mover fronteras directamente desde el texto coloreado del rango madre. El mapa superior pasó a ser realmente editable: cada union entre colores expone un handle draggable que reasigna ownership, recoloca el corte activo, limpia fronteras internas absorbidas y persiste el resultado como parte de la sesión de división. Con esto el flujo deja de depender solo de trims en timeline para ajustar dónde empieza y termina cada subsecuencia.
- 2026-03-31: El estudio de `Sugerir dividir` dejó de construirse como una suma de bloques internos de cada propuesta y pasó a montar una división semántica viva por subsecuencia sugerida. Eso limpia la timeline del split studio para que cada color y cada bloque correspondan a una sola división editable del rango madre, hace más coherentes `C` y `Fundir`, añade métricas de divisiones activas en la consola superior y además oculta acciones destructivas del editor genérico que no encajan con este flujo, como borrar la secuencia temporal completa o eliminar un clip suelto desde el timeline.
- 2026-03-31: `Sugerir dividir` dio el primer salto hacia el flujo nuevo pedido. En vez de sentirse como una revisión enterrada en el editor de abajo, ahora el split review se monta como un estudio dedicado en overlay fijo sobre la app, reutilizando transcript, preview y timeline reales, con una consola superior de corrida que deja visible el rango enviado, la traza reciente y las respuestas OpenAI resumidas. Todavía no es el modal madre-hijas final ni la nueva timeline semántica por fronteras de texto, pero ya deja explícito que se abrió un modo distinto de trabajo para dividir una sugerida.
- 2026-03-31: `Sugerir dividir` ahora fuerza scroll suave hacia el workspace del editor cuando abre la revisión temporal. Antes el flujo podía activarse correctamente pero quedar fuera de viewport, dando la sensación de que el botón no había hecho nada. Esto no reemplaza todavía el modal dedicado que falta construir, pero evita el falso negativo visual del flujo actual.
- 2026-03-31: Se corrigió la diferencia de comportamiento entre una secuencia normal y `Abrir rango` de una sugerida. La review temporal de sugeridas se estaba regenerando desde `editorialAgentWorkspaceSeedReview` en cada render del workspace, y ese rebuild volvía a poner selección y playhead en el primer bloque. Ahora, igual que en split review, la secuencia temporal ya existente se conserva mientras no haya una regeneración explícita (`savedSequence`), así que el scrub y el posicionamiento manual del preview dejan de saltar al inicio solo por estar en una revisión temporal.
- 2026-03-31: Se corrigió una desincronización entre el estado del playhead y su `ref` en el editor principal. El scrub de la timeline, el click sobre clips/palabras y varios cues de reproducción ya no dependen de que React alcance a renderizar antes del siguiente gesto: ahora actualizan ambos a la vez. Con eso deja de aparecer el síntoma de “vuelve al inicio” al intentar posicionar la barra antes de reproducir o al adelantar manualmente el preview.
- 2026-03-31: `Abrir rango` de una sugerida dejó de heredar los `blocks` y `break_after_word_ids` internos del candidato guardado. Antes podía abrirse como una pseudo-secuencia con decenas de cortes y parecer dividida casi palabra por palabra. Ahora ese modo entra como un único clip base del rango completo, que es el punto correcto para revisar o volver a subdividir.
- 2026-03-31: `Descartar` dentro de `Abrir rango` ya no deja viva la secuencia temporal de revisión. Si la revisión abierta corresponde a una sola sugerida (`saved-suggestion-review`), el descarte ahora cierra y limpia todo el workspace temporal; al volver a abrir esa sugerida se reconstruye otra vez desde su rango original, no desde el estado tocado que acababa de descartarse.
- 2026-03-31: `Abrir rango` de una sugerida volvió a comportarse como reset limpio al rango original. En vez de reusar a ciegas la misma secuencia temporal si ya estaba abierta, ahora fuerza una revisión nueva desde el payload base de la sugerida y además el editor expone `Restaurar rango` dentro de la revisión para volver a ese punto de partida sin salir del workspace. Esto deja mucho más claro desde dónde empezar otra subdivisión.
- 2026-03-30: Se endureció el criterio de `Abrir división`. El frontend ya no toma cualquier entrada vieja de `localStorage` como si fuera una división válida: para mostrar `División guardada` y reabrirla ahora exige una sesión realmente restaurable con payload y secuencia temporal coherentes. Si la entrada quedó incompleta o huérfana, se limpia y la sugerida vuelve a ofrecer `Sugerir dividir` en vez de abrir por error otra secuencia creada que no corresponde.
- 2026-03-30: `Salir` en el editor de secuencia ahora limpia también la secuencia activa, no solo oculta el workbench. Con eso la tarjeta deja de quedar marcada como abierta después de cerrar, y al volver a hacer click se reabre desde cero abajo. En la misma lógica, si se borra la secuencia activa ya no se autoabre otra por fallback: el editor queda sin secuencia abierta hasta que el usuario elija una.
- 2026-03-30: Se rehizo la gestión de bandejas para que `Secuencias creadas` y `Secuencias sugeridas` compartan la misma lógica mental: click normal abre, `Seleccionar` activa multiselección y recién entonces aparece un único botón de papelera para borrar lo elegido. En esa misma limpieza se retiraron `Borrar todas`, `Borrar seleccionada` y `Guardar seleccionada como secuencia` de la cabecera de sugeridas.
- 2026-03-30: La acción de cerrar una secuencia abierta dejó de estar escondida en la cabecera de la bandeja y pasó al header del preview, junto a `Undo/Redo`, como `Salir`, para que quede en el mismo lugar donde el usuario ya espera acciones de contexto sobre la vista activa.
- 2026-03-30: La bandeja de `Secuencias creadas` ahora permite elegir secuencias persistidas de forma explícita y borrarlas en lote desde la cabecera. Las tarjetas mantienen su acción normal de abrir en el editor, pero suman un control `Elegir/Quitar` pensado solo para borrado, sin mezclar esta selección con las sugeridas ni con la selección de palabras del transcript.
- 2026-03-30: Se blindó la apertura del workspace de review para que no recicle la secuencia temporal cuando cambia la sugerida o división origen. Antes podía reutilizar el mismo `tempSequenceId` entre aperturas consecutivas y, si cierta regeneración se saltaba, eso abría la puerta a ver clips de una revisión anterior dentro de otra. Ahora cada origen nuevo monta su propia secuencia temporal aislada.
- 2026-03-30: Se corrigió el flujo de revisión de `Abrir división` en frontend para que `Restaurar desde IA` vuelva a la base guardada sin relanzar una nueva corrida pagada. En la misma ronda, `Delete/Backspace` ya puede eliminar el clip seleccionado del timeline desactivando su texto al instante, `C` deja de repetir cortes por auto-repeat del teclado, la aprobación individual de subsecuencias evita duplicados y el strip superior de división deja de contar/renderizar candidatas vacías que inflaban el `N de M` y mostraban "tramos extra" fantasma.
- 2026-03-30: El módulo superior de revisión dentro de `Sugerir dividir` ahora se puede minimizar con una pestaña propia usando doble click o arrastre vertical. El bloque de `División en el editor real` se colapsa a un resumen corto con animación, libera altura para transcript y preview, y el divider inferior vuelve a usarse solo para redimensionar el workspace y la timeline.
- 2026-03-30: Se comprimió todavía más el estado minimizado del banner superior de revisión. El subtítulo del fragmento se oculta al plegar, las pills y paddings bajaron de tamaño y el control de abrir/cerrar pasó a la misma fila del resumen, para que el bloque ocupe prácticamente una sola banda horizontal.
- 2026-03-30: En el workspace de revisión quedaron unificados los controles de historial solo en el header del preview y el botón intermedio del playerbar pasó a cortar el clip en el playhead usando la misma ruta que el atajo `C`. También el modal de ampliar rango dejó de bloquear párrafos por candidatas vecinas que ya no tienen bloques activos en el split review, así que el texto “apagado” por otra subsecuencia vuelve a poder absorberse desde la actual.
- 2026-03-30: Se corrigió el caso en que `Ajustar rango` regeneraba una secuencia visible pero no dejaba rastro en `Undo/Redo`. Ahora la regeneración empuja una nueva versión de la secuencia temporal del workspace dentro del historial del editor. En paralelo, el bloqueo del modal de rango dejó de decidir por solape temporal bruto y pasó a usar `word_ids` realmente activos por candidata, para que una vecina pueda ceder texto desactivado al clip anterior o posterior.
- 2026-03-30: Se corrigió otra fuga del modo `split-review`: al ajustar un rango, el frontend estaba reconstruyendo el workspace como si fuera un fragmento aislado y no una división completa. Ahora preserva el modo `split-suggestion-review`, mezcla la candidata ajustada con las vecinas activas y recompone la secuencia temporal desde los bloques reales de cada candidata, no desde rangos aproximados `start/end`. Eso deja mucho más consistente el ownership de color, las absorciones de texto liberado y las operaciones posteriores de fundir/cortar.
- 2026-03-30: El ownership visual del split review dejó de recalcularse solo por mayoría de `word_ids` en cada render. Los bloques ahora conservan primero su candidata previa mientras sigan siendo el mismo bloque del editor, y recién caen al heurístico de overlap como fallback. Con eso, al estirar el borde derecho de una subsecuencia el texto absorbido deja de “seguir morado” solo porque antes pertenecía a la vecina.
- 2026-03-30: Se reforzó el split review con ownership persistente por `word_id` dentro de la secuencia temporal. Los trims y `Fundir` ya no se limitan a activar/desactivar palabras: también reasignan esas palabras a la candidata que las absorbe, ese mapa se serializa en el save state local y al recargar la división ya no debería reaparecer un bloque azul/original en medio solo por reconstrucción heurística.
- 2026-03-30: Se limpió otra fuga del trim en split review: cuando un clip reactivaba texto absorbido, podían sobrevivir `breakAfterWordIds` y `customSplitPoints` del corte original dentro de ese tramo, creando un clip intermedio glitch aunque el ownership ya hubiera pasado al receptor. Ahora esos cortes internos se eliminan al absorber, para que el drag extienda un único clip continuo.
- 2026-03-30: Se reforzó el guardado de la división activa frente a refresh inmediato. Además del autosave por efecto, al terminar un drag relevante del split review ahora se escribe de inmediato el snapshot actual en `localStorage`, y también se fuerza un guardado síncrono en `beforeunload`. Eso evita perder el último trim/reordenamiento si el usuario refresca enseguida.
- 2026-03-30: Se corrigió otra carrera de persistencia del split review. El save inmediato dejó de depender de `refs` que se actualizaban en el render siguiente: ahora el trim final y el reorder persisten usando la `nextSequence` calculada dentro del propio `updateActiveSequence`, y se retiró un `setTimeout` suelto que podía escribir snapshots fuera de momento.
- 2026-03-30: El save state del split review ahora persiste también las candidatas visibles/editadas del workspace, no solo la secuencia temporal. Antes, al refrescar, la timeline podía volver bien pero las tarjetas superiores se reconstruían desde `alternatives` originales de IA y daban la sensación de que nada se había guardado. Ahora ambos niveles se reabren desde el estado editado.
- 2026-03-30: Se endureció todavía más la restauración del split review. Al guardar ya no se reutilizan las candidatas visibles del render actual, sino que se reconstruyen directamente desde la `sequenceSnapshot` serializada y su `splitWordOwnership`. Eso evita que un refresh caiga otra vez en el valor original de IA por una carrera entre el drag final y el render de las tarjetas.
- 2026-03-30: Se evitó que los tramos apagados por el usuario reaparezcan como un clip nuevo al recargar. El scope persistido del split review ahora guarda `suppressed_gap_word_ids` desde `inactiveWordIds`, y la reconstrucción de `gaps` deja de considerar esas palabras como “no cubiertas recuperables”.
- 2026-03-30: Se añadió una vía explícita de recuperación para divisiones guardadas rotas. `Volver a sugerir` ahora limpia antes la sesión local de split review cuando fuerza regeneración, y el workspace expone `Restaurar desde IA` para descartar la división local actual y reconstruir una nueva desde el análisis del backend.
- 2026-03-30: Se corrigió una regresión de refresh en sugeridas. Cuando existía un batch real del backend, el efecto de saneamiento estaba quitando del `bootstrap` las alternativas cuyo id también existía en localStorage; como normalmente eran duplicados del mismo batch, la lista podía quedar vacía tras recargar. Ahora ese saneamiento solo limpia el cache local redundante y deja intactas las sugeridas visibles.
- 2026-03-30: Se blindó el flujo `Sugerir temático` para no perder costo pagado a IA. Antes devolvía secuencias al frontend y este las convertía en sugeridas solo en memoria/localStorage; tras refresh podían desaparecer. Ahora el backend persiste ese lote como `AISuggestion` activo, devuelve `bootstrap` actualizado y el frontend hidrata desde esa versión durable en vez de fabricar ids locales efímeros.
- 2026-03-30: Se añadió aprobación directa desde la bandeja de sugeridas. Ahora una sugerida seleccionada se puede guardar como secuencia real sin abrir antes el modo de revisión, y si esa sugerida ya había sido aprobada antes el editor enfoca la secuencia existente en vez de duplicarla.
- 2026-03-30: Se añadió recuperación desde historial IA para sugeridas temáticas. El backend ahora lista logs `thematic-segmentation` recuperables y puede reinyectarlos como `AISuggestion` activo; el frontend expone `Ver historial IA` y `Restaurar lote IA` desde la bandeja de sugeridas para traer de vuelta lotes anteriores sin rerun pagado.
- 2026-03-30: Se corrigió el staging del workspace durante `Sugerir dividir`. Antes el editor entraba en modo split con el rango original incluso antes de recibir las subsecuencias finales, y una guarda impedía regenerar la secuencia temporal cuando llegaba la respuesta real; eso hacía que durante el análisis se viera un armado engañoso y, al terminar, el texto pareciera mezclado entre clips. Ahora la espera queda en modo review normal y la transición a split regenera correctamente la secuencia temporal final.
- 2026-03-30: Se corrigió la persistencia de `Fundir con anterior/siguiente` dentro del split review. Ese camino actualizaba la secuencia temporal pero no disparaba el snapshot inmediato usado para restaurar tras refresh; trims y reorder sí lo hacían. Ahora el merge persiste al instante el split review actualizado, evitando perder unificación de segmentos si se recarga enseguida.
- 2026-03-30: Se corrigió el cambio entre sugeridas abiertas y se expuso un cierre visible en la bandeja. `handleOpenSavedSuggestedSequences` ahora pasa siempre el `sourceCandidateId` de la sugerida que se abre, evitando que el workspace reutilice el id de la revisión anterior, y la cabecera de sugeridas muestra `Cerrar revisión abierta` para salir sin depender del botón enterrado dentro del banner superior.
- 2026-03-30: Se añadió también un cierre visible para secuencias normales. La cabecera de `Secuencias creadas` ahora expone `Cerrar secuencia abierta`, que cierra la vista actual del editor sin borrar la secuencia ni depender de minimizar paneles secundarios.
- 2026-03-30: Se ajustó la geometría base del workspace del editor para que se comporte como el editor real también en revisión/split. El `editor stack` ahora puede estirarse para llenar la altura disponible, el pane superior reparte mejor la vertical entre transcript y preview, el timeline queda anclado al fondo en vez de dejar hueco muerto debajo y el stage del preview deja de forzar `height: 100%`, con lo que el cambio de relación de aspecto vuelve a modificar realmente la forma visible del canvas.
- 2026-03-30: Se compactó la capa de revisión de `Sugerir dividir` para que no tape visualmente el editor real. El mapa del rango sugerido pasó a un `details` colapsado por defecto, el banner superior deja explícito que transcript/preview/timeline/historial siguen siendo la misma instancia del editor y los botones `Undo/Redo` también quedaron visibles en el header del preview, no solo en el transcript o en la barra de revisión.
- 2026-03-30: Se corrigió el colapso del editor de preview dentro de `Sugerir dividir`. La columna derecha seguía montándose, pero su panel principal podía quedar con altura cero y solo dejaba visible `Más opciones`; ahora la columna usa filas auto y el panel de preview tiene una altura mínima intrínseca, para que el editor aparezca aunque el workspace superior esté en scroll o el banner de revisión sea alto.
- 2026-03-30: Se reforzó la visibilidad del preview del editor real. El panel principal de preview ahora reserva una altura mínima explícita y el stage usa ancho completo para evitar colapsos silenciosos del canvas en la revisión de división. En paralelo, `WorkflowTimelinePanel` y `WorkflowTimelineClipLane` quedaron blindados contra props `undefined` en runtime dev/HMR, porque una caída temprana del timeline podía dejar el editor a medio render aunque el build productivo siguiera pasando.
- 2026-03-30: Se evitó que el workspace de `Sugerir dividir` reinyecte la secuencia temporal después de un trim cuando esa secuencia ya estaba abierta, que era lo que hacía que el recorte pareciera aplicarse durante el drag pero se recompusiera al soltar. En la misma ronda también se inicializó el preview editorial paralelo dentro del workspace review, no solo en el modal, para que ese preview vuelva a mostrar el primer bloque del corte activo al abrir la división.
- 2026-03-30: En `Sugerir dividir`, los trims hechos sobre la timeline del workspace ahora también recalculan la candidata visible de revisión. Las chips, tarjetas, duración, bloques mini-timeline y previews textuales dejan de leer solo el snapshot original de IA y pasan a reconstruirse desde los bloques reales editados del editor; además el rebuild de la review respeta `source_in/source_out` de cada bloque cuando existen, para no perder recortes precisos al regenerar el modelo de revisión.
- 2026-03-30: La división guardada de `Sugerir dividir` ahora persiste también como secuencia editada del workspace y no solo como payload de IA. Al reabrir una división se restauran bloques recortados, clip activo, selección del timeline y posición de reproducción; además las tarjetas que ya tienen división dejan de ofrecer `Abrir rango`, pasan a priorizar `Abrir división` y exponen `Volver a sugerir` por separado. En la revisión dentro del editor también quedaron visibles los controles `Undo/Redo` en la barra de acciones para mantener la misma dinámica de edición fina.
- 2026-03-29: El modo `Sugerir dividir` ahora persiste por proyecto y por sugerida origen. Si ya existe una división generada, el botón principal reabre esa división guardada en vez de relanzar IA; además aparece `Volver a sugerir` para recalcular sin perder lo ya aprobado o generado antes. Al refrescar, el proyecto reabre la última división activa. También se agregan tramos no cubiertos por la IA dentro del rango original como propuestas recuperadas (`Parte inicial no cubierta`, etc.) para que el editor no deje afuera fragmentos del rango madre.
- 2026-03-29: El modo `Sugerir dividir` dentro del editor ya no reconstruye solo la propuesta activa. La secuencia temporal de revisión ahora puede cargar las subsecuencias sugeridas como bloques reales simultáneos dentro del timeline, con color por bloque en timeline/transcript, selección sincronizada entre chips y clips, aprobación basada en los bloques visibles del workspace y scroll vertical restaurado para volver a ver el preview cuando el banner de revisión crece.
- 2026-03-29: Se corrigió el crash del split review cuando una sugerida local traía `content_score` textual como `medium`. El backend ahora normaliza scores léxicos y `Sugerir dividir` abre el editor abajo desde el inicio del análisis: muestra estado de IA inline, mapa del rango coloreado por subsecuencia sugerida, selección de propuesta dentro del mismo workspace y botón `Aprobar división` para crear todas las secuencias derivadas de una vez.
- 2026-03-29: Se corrigió el arranque visual de `Sugerir dividir`. Al hacer click ya no queda solo el botón en `Analizando...`: el frontend vuelve a abrir feedback inmediato del progreso del agente, y el backend ahora detecta corridas editoriales atascadas (`processing` zombie), las invalida y permite relanzar la acción en vez de reutilizar un `run_id` muerto.
- 2026-03-29: La revisión de sugeridas y de `Sugerir dividir` dejó de depender solo del modal editorial. Ahora cada propuesta puede abrirse como una secuencia temporal dentro del editor principal: transcript final a la izquierda, preview y timeline reales, trims y ajustes de clip con las mismas herramientas del editor, y sin persistirse como secuencia definitiva hasta aprobarla.
- 2026-03-29: Se añadió la acción `Sugerir dividir` sobre una sugerida guardada. Dispara una nueva pasada OpenAI textual sobre ese rango, analiza si el bloque se entiende solo, si se puede separar en clips de `20/40/60s`, propone subsecuencias revisables en el mismo modal editorial, agrega `play` por propuesta y las secuencias aprobadas quedan marcadas visualmente como subsecuencias sugeridas derivadas del rango original.
- 2026-03-29: La terminal del agente en modo `saved-suggestion-review` ahora deja explícito que abrir una sugerida guardada no dispara una segunda pasada de IA. También muestra métricas del rango reconstruido (`duración`, `palabras`, `bloques`, `cortes`) y una línea concreta sobre cómo lanzar de verdad otra pasada (`Depurar`, `Ampliar rango y redepurar` o `adjust-suggestion`).
- 2026-03-29: Se corrigió un bug en `Sugerir` temático: el frontend estaba marcando el lote local `thematic-batch-*` como si fuera un batch canónico del backend y purgaba de inmediato todas las alternativas locales, dejando el resumen visible pero la lista vacía. Ahora solo se considera `backendBatchId` cuando el id de sugeridas es persistido real del servidor.
- 2026-03-29: El modulo de costos OpenAI del header ahora se actualiza en vivo mientras corre `Sugerir`: `Proyecto` y `Mes` reflejan el consumo incremental del polling actual, y aparece una tarjeta temporal `Corrida` con costo, tokens e iteraciones del run activo.
- 2026-03-29: El modal del agente editorial ahora recibe en vivo las respuestas OpenAI de la corrida activa (`operation_name`, costo, tokens y `summary` extraído del preview guardado), para que al usar el botón `Sugerir` se pueda leer en frontend y también reproducir el mismo formato en terminal/chat sin esperar al resumen final.
- 2026-03-29: Cada secuencia sugerida ahora persiste `title_proposals` con 3 variantes cortas de titulo, junto con `selected_title_index`. El backend recalcula `main_title`, `video_title`, `title_reason` y `title_coverage_explanation` desde la opcion activa, y el dashboard ya permite elegir entre esas propuestas y guardar la seleccion.
- 2026-03-29: `video_title` y `main_title` ahora se normalizan a un formato corto para publicacion: idealmente entre 5 y 10 palabras, con recorte automatico y limpieza de cierres pobres cuando OpenAI devuelve hooks demasiado largos.
- 2026-03-29: El pipeline ahora puede subdividir cada contexto padre en secuencias internas usando solo el rango seleccionado mas la memoria contextual de la primera pasada. Cada secuencia persiste `video_title`, `title_reason` y `title_coverage_explanation` para anclar el hook al contenido real del script y justificar donde se cumple esa promesa.
- 2026-03-29: En modo barato, cuando `judge-pass` esta desactivado y un `segment_group` ya fue disciplinado contra `context_map`, el `candidate-pass` deja de relanzar discovery OpenAI por ese bloque y conserva el contexto completo como seed. Esto evita perder contextos enteros en la fase barata y reduce costo por corrida.
# Bitácora de Trabajo

## Propósito

Registrar qué se hizo, qué cambió, qué quedó pendiente y qué decisión nueva se tomó. Este archivo debe crecer cronológicamente.

## Formato recomendado por entrada

### Fecha

`YYYY-MM-DD`

### Trabajo realizado

- Punto 1
- Punto 2

### Decisiones tomadas

- Decisión

### Bloqueos o riesgos

- La interacción actual replica un resaltado por arrastre entre párrafos, no una selección literal palabra a palabra; si más adelante hace falta granularidad más fina, habrá que bajar del nivel párrafo al nivel `word span` sin romper la regla de contigüidad.

### Próximo paso

- Probar manualmente si esta selección por barrido ya se siente suficientemente natural o si conviene avanzar a un resaltado todavía más fino dentro de cada párrafo.

---

### 2026-03-29 (Recorte de costo editorial OpenAI)

#### Trabajo realizado

- Se redujo el techo de macrosegmentos OpenAI de `48` a `24` y el techo de bloques revisados en `candidate-pass` de `48` a `18`.
- Se incorporo un cap explicito de `1600` palabras para las fases globales `segment-map` y `context-map` cuando construyen payload para OpenAI.
- Se recorto el `context_map` reenviado a `segment-map` para mandar menos observaciones y menos contextos resumidos.
- Se desactivo por defecto la fase OpenAI de `context-map` y se dejo el mapa descriptivo local como fuente temporal para evitar duplicar un pase global muy caro antes de segmentar.
- Se evito llamar a OpenAI en `candidate-pass` para macrosegmentos que no requieren subdivision; en esos casos se conserva el `seed` conversacional y se delega el ajuste fino al `judge-pass`.
- Se redujo el conjunto de `focus_word_ids` enviado en discovery por macrosegmento de `500` a `220`.
- Se reemplazaron las dos llamadas OpenAI globales separadas (`context-map` + `segment-map`) por una sola llamada combinada que devuelve `observations`, `contexts` y `segment_groups` en el mismo JSON.
- Se agrego un resumen persistido por corrida con `input_tokens`, `output_tokens`, `total_tokens`, costo estimado total y desglose por operacion OpenAI dentro de `structured_response.openai_usage`.
- Se incorporo la tarifa de `cached input` para `gpt-5.4` (`$0.25 / 1M`) y ahora los calculos de costo distinguen `input_tokens`, `cached_input_tokens`, `uncached_input_tokens` y `output_tokens` tanto por corrida como a nivel proyecto.
- Se desactivaron temporalmente `judge-pass` y `visual-guide-pass` para permitir corridas baratas que persistan un batch completo con `openai_usage` sin disparar el costo por candidato.
- En modo barato, `contexts` pasaron a ser la unidad base de verdad: si la segmentacion fina repite o fragmenta sin evidencia clara, el sistema colapsa de nuevo al contexto y filtra microcortes antes de persistir sugeridas.

#### Decisiones tomadas

- El costo se tiene que atacar en la estructura de la canalizacion, no solo en prompts: menos pasadas globales y menos llamadas por bloque.
- Mientras no haya una version compacta y verificadamente barata del `context-map` con OpenAI, conviene preservar la arquitectura contexto-primero usando fallback local persistente.
- La recomendacion adoptada para esta etapa es una sola lectura OpenAI del transcript util que resuelva a la vez el mapa descriptivo y la segmentacion editorial base.
- La API usada devuelve `usage` de tokens pero no un costo USD oficial por respuesta; por eso el backend ahora deja explicito que el costo publicado es una estimacion calculada con la tarifa configurada en `.env`.
- Los totales del proyecto y los resúmenes editoriales ya no deben tratar todo el prompt como input normal cuando OpenAI informa `cached_tokens`; eso inflaba artificialmente el costo.
- Mientras estemos validando segmentacion base y costo por corrida, conviene privilegiar la persistencia del batch barato antes que el juicio fino y la memoria visual por candidato.
- Si `judge-pass` esta apagado, no conviene dejar que `segment_groups` fragmente libremente una misma entrevista; primero debe respetarse la cobertura de contexto y luego, solo si hay cortes claros, aceptar subdivisiones.

#### Bloqueos o riesgos

- Al bajar el numero de bloques revisados por OpenAI, algunos subtemas de prioridad menor pueden quedar fuera del primer batch y requerir una segunda pasada o un criterio de ranking mejor.
- El cap global de palabras reduce costo, pero puede degradar algo la lectura de contexto en transcripts muy largos si el texto util queda demasiado repartido.

#### Proximo paso

- Ejecutar otra corrida editorial real sobre Jimny y medir si ahora aparece un batch nuevo persistido con costo y latencia razonables.

### 2026-03-26 (Iteracion UI shell)

#### Trabajo realizado

- Se rehizo la capa superior del editor (`workflow shell`) con un layout nuevo de `mesa de control`, separando identidad del proyecto, telemetria OpenAI y acciones de trabajo en bloques visuales distintos.
- Se mantuvieron intactos los contenidos internos de los modulos de script, secuencias, preview y timeline; el cambio se concentro en la composicion por encima de esos paneles.
- Se alinearon los minimos de altura del script en drag + normalizacion + CSS para evitar que el divisor se perciba fijo por un clamp inconsistente.
- Se estabilizo el arrastre de la cortina del script eliminando la captura de puntero local y fijando listeners globales estables, para evitar que el drag se corte o quede aparentemente encerrado tras abrir un proyecto.
- Se agrego control directo de altura en la propia barra del script (`-`, slider, `+` + lectura en px) como fallback funcional inmediato cuando el arrastre del mouse no responda bien en ciertos equipos/navegadores.
- Se reescribio la arquitectura del bloque conflictivo `Script principal + divisor + Secuencias` a un split vertical real con `grid` y altura acotada, para eliminar el comportamiento de contenedor "encerrado" que impedia subir/bajar de forma coherente.
- Se retiro el control extra (`slider` y botones) de la barra divisora para volver a una interaccion limpia por arrastre.
- Se elimino el tope rigido que frenaba la altura del script alrededor de ~430px; ahora puede crecer y empujar hacia abajo con scroll general.
- Se compacto la zona superior de proyecto/telemetria para ahorrar altura y se redujo el diametro de bordes redondeados en paneles y bloques de cabecera.
- Se rehizo la cabecera superior en un modulo unico y compacto (sin subcajas/divisores internos), priorizando `Proyectos` y colocando `Abrir/Minimizar edicion fina` junto a ese boton.
- El nombre del proyecto quedo en la misma linea principal y la segunda linea agrupa badges de estado + costos OpenAI en formato mas plano y de menor altura.
- Se elimino el texto auxiliar `Editor por transcript` y el titulo del proyecto quedo junto a los botones de accion en la misma linea.
- Se igualo la medida visual de `Proyectos` y `Abrir/Minimizar edicion fina` para que operen como pareja de controles.
- Se quito la leyenda superior de `Script principal` ("Selecciona desde...") y se redujo el separador/espaciado del encabezado del panel para ganar altura util.
- Se habilito scroll vertical maestro en el shell del editor para que la cortina del script pueda seguir bajando y empujar contenido sin quedar atrapada por viewport fijo.
- El panel `Script principal` dejo de usar header separado del componente base y ahora muestra titulo + minimizar/expandir en linea interna compacta junto al flujo de acciones/metricas.
- Se amplió la visibilidad del `Terminal agente` aumentando su peso en la columna derecha y reduciendo altura maxima del preview superior para priorizar lectura en modo vertical.
- Se reescribio el contenedor `Script + cortina + Secuencias` desde un split de grid a flujo vertical simple, para que `Secuencias creadas/sugeridas` no impongan un limite estructural al desplazamiento de la cortina.
- Se elevaron los topes de altura del script en runtime y en normalizacion persistida (`20000px`) para eliminar el limitador practico durante el arrastre.
- El editor ahora arranca por defecto con paneles colapsados (`Script principal`, `Secuencias creadas` y `Edicion fina`) cuando no hay preferencia previa guardada.
- Se agrego persistencia por proyecto para el estado de colapso de tarjetas de `Secuencias sugeridas` (todo colapsado, todo expandido o personalizado por tarjeta), respetando setting individual cuando exista.

#### Decisiones tomadas

- Cuando la percepcion de la pagina es "mal hecha", conviene redisenar primero la arquitectura visual global y no retocar detalles menores dentro de modulos que ya cumplen su funcion.
- Los limites de layout deben definirse con el mismo piso en logica de interaccion, persistencia y estilos para evitar comportamientos contradictorios.

#### Proximo paso

- Validar en navegador que la nueva cabecera se lee mejor en desktop y mobile, y confirmar manualmente que el divisor del script responde en toda la carrera de arrastre.

### 2026-03-27 (Subdivision de segmentos largos + objetivos 15-40s)

#### Trabajo realizado

- Cambio de `EDITORIAL_TARGET_DURATIONS` de `[14, 22, 32, 45, 60]` a `[15, 20, 30, 40]` para reflejar el objetivo del usuario: clips de 20 y 40 segundos.
- Refactorizacion de `_build_editorial_candidates_for_segment_group`: ahora devuelve una lista en vez de un solo candidato.
- Para grupos de duracion > 35s: `max_alternatives_for_group = 2` y la instruccion al AI ordena explicitamente "propone 2 clips de 15-40s si hay subtemas con punto de corte claro".
- Para grupos <= 35s: comportamiento identico al anterior (una sola alternativa).
- Si el AI devuelve 2 candidatos (ai_subdivided), se aceptan ambos directamente sin aplicar el guard de `blocked_pruning` (ya que la subdivision fue intencional).
- Actualizacion del caller en `generate_ai_suggestion`: cambia de `append(proposed_candidate)` a `extend(group_candidates)` para manejar 1 o 2 candidatos por grupo.
- Actualizacion de `C3_duracion_valida` en el payload del AI: ahora referencia el rango objetivo 15-40s y exige justificacion si el clip supera 40s.
- `manage.py check`: 0 errores. `python -m py_compile services.py`: OK.

#### Decisiones tomadas

- Las sugerencias largas (#8 45.7s, #11 45.5s, #28 44.8s, #30 45.6s) eran el resultado natural de `max_alternatives=1` por grupo mas objetivos que incluian 45 y 60s como validos.
- La solucion es habilitar subdivision a nivel de macrosegmento (no de discovery global), con el AI recibiendo instruccion directa y metas de duracion precisas.

#### Proximo paso

- Correr un nuevo scan editorial para verificar que los segmentos largos ahora producen 2 clips cortos en lugar de 1 largo.

---

### 2026-03-27 (Evaluacion sugeridas #35+)

#### Trabajo realizado

- Se preparo una nueva ronda de evaluacion para `Secuencias sugeridas`, tomando como base historica las 34 ya revisadas.
- Desde esta corrida, toda sugerida nueva se interpreta y evalua a partir del indice `#35` en adelante.

#### Decisiones tomadas

- El criterio de evaluacion se aplicara solo sobre candidatas nuevas (`#35+`) para no mezclar resultados con rondas anteriores.
- Si una sugerida vieja reaparece con ajustes, se marca como "revision"; si aparece contenido/rango nuevo, se marca como "nueva".

#### Proximo paso

- Ejecutar corrida nueva de sugerencias, etiquetar cada tarjeta como `nueva/revision` y cerrar la ronda con lista priorizada de las mejores candidatas para abrir rango.

### 2026-03-27 (Migracion tematicas creadas -> sugeridas)

#### Trabajo realizado

- Se corrigio la herencia del bug antiguo donde algunas secuencias tematicas ya generadas aparecian en `Secuencias creadas` en lugar de `Secuencias sugeridas`.
- El frontend ahora detecta drafts con id `thematic-*`, los reconstruye como candidatas sugeridas y los saca automaticamente del bloque de creadas.
- Las sugeridas tematicas locales ahora se guardan por proyecto en `localStorage`, de modo que no se pierdan al recargar y que la migracion siga siendo estable entre sesiones.
- Al abrir proyecto, el bootstrap mezcla las sugeridas del backend con las sugeridas locales persistidas y elimina duplicados por id.
- El flujo `Sugerir tematico` y el borrado de sugeridas quedaron sincronizados con ese almacenamiento local para no reintroducir incoherencia entre UI y estado persistido.

#### Decisiones tomadas

- La heuristica de migracion se limita a ids `thematic-*` para no tocar secuencias creadas manualmente ni drafts editoriales aprobados.
- Como las sugeridas tematicas de este flujo no nacen persistidas en backend, conviene persistirlas localmente por proyecto hasta definir una capa de guardado server-side especifica.

#### Proximo paso

- Verificar manualmente en el proyecto actual que las creadas legitimas permanecen arriba y que las tematicas heredadas reaparecen dentro de `Secuencias sugeridas` despues de recargar.

### 2026-03-27 (Scan editorial sin cupo fijo)

#### Trabajo realizado

- Se eliminó del pipeline editorial la idea de `target_count` como objetivo de cantidad para el scan principal de sugeridas.
- El prompt de discovery ya no pide `exactamente N` alternativas; ahora pide entre `0` y un tope técnico, dejando explícito que no debe rellenar cupos si no hay conversaciones cerradas suficientes.
- El recorrido principal del scan pasó a operar en orden cronológico sobre los macrosegmentos detectados, en vez de priorizar primero por score y empujar una cantidad estimada por duración.
- El prompt de segmentación y el prompt de discovery se reforzaron para trabajar como edición lineal: primero entender el contexto completo del alcance, luego agrupar por conversación/subtema, y solo después definir secuencias.
- El payload persistido de sugeridas ahora guarda como `target_count` la cantidad real detectada, no una meta heredada por duración del scope.

#### Decisiones tomadas

- El scan editorial no debe comportarse como un buscador de `N clips`, sino como una lectura lineal del contenido que separa rangos válidos y los deja listos para depuración posterior.
- La cantidad de sugeridas pasa a ser un resultado del análisis, no una consigna editorial para el modelo.
- Se mantiene un tope técnico de seguridad para no abrir lotes infinitos, pero deja de existir un cupo narrativo impuesto al contenido.

#### Proximo paso

- Ejecutar una nueva corrida editorial real y revisar si baja la cantidad de sugeridas forzadas, redundantes o mezcladas entre temas vecinos.

### 2026-03-27 (Limpieza sugeridas 34-58)

### 2026-03-27 (Sugerir sin descarte + juez con division)

#### Trabajo realizado

- Se ajusto la filosofia del scan editorial para que la fase de sugerir no descarte conceptos validos por perseguir la duracion final de publicacion.
- El `segment_map` y el `candidate-pass` ahora dejan explicito en prompt que `20s` y `40s` son referencias de depuracion posterior, no limites rigidos del discovery.
- Los macrosegmentos se marcan antes como potencialmente divisibles (`>= 38s` o multiples transiciones), para abrir antes la puerta a separar ideas distintas.
- El `candidate-pass` ahora pide 1 o 2 secuencias por macrosegmento segun estructura, permitiendo conservar una sola idea completa de `45-60s` cuando todavia deba perfilarse en depuracion.
- El `judge-pass` dejo de estar obligado a devolver exactamente una alternativa: ahora puede devolver `1` o `2` alternativas cuando detecta dos ideas con entrada y cierre propios.
- El `judge-pass` se reforzo para decidir entre `mantener`, `perfilar` o `dividir`, en vez de empujar recortes prematuros.
- La normalizacion de `target_duration_seconds` se alineo a bandas `20/30/40/60` en lugar de las bandas antiguas `14/22/32/45`.
- `manage.py check` y `py_compile` pasaron sin errores tras la refactorizacion.

#### Decisiones tomadas

- En la fase de sugerir, el sistema debe segmentar y perfilar, no podar agresivamente contenido valido.
- El descarte fino y el ajuste a la duracion final pertenecen a la fase de depuracion, no al discovery inicial.
- Una sugerida puede quedar por encima de `40s` si representa una sola idea completa que luego pueda apretarse sin perder el concepto.
- Si un rango contiene dos ideas autosuficientes, el juez debe poder dividirlo inmediatamente en dos sugeridas.

#### Proximo paso

- Ejecutar una nueva corrida editorial y comprobar si los bloques largos ahora se comportan de dos maneras correctas: o se dividen en dos piezas, o se conservan como una sola pieza refinable sin perder conceptos.

### 2026-04-10 (Slide 5 de análisis profundo + prompt versions)

#### Trabajo realizado

- Se añadió un endpoint nuevo `GET/POST /api/editor/projects/<id>/deep-analysis/` para consolidar por proyecto: entrada real enviada a la IA, salida depurada y resultado final editado manualmente, manteniendo además el origen completo de la secuencia cuando difiere.
- El dataset de análisis ahora excluye secuencias transitorias de revisión y considera comparables las corridas persistidas que tengan entrada enviada a IA, salida depurada y estado final persistido, aunque la secuencia no haya quedado marcada como `applied`.
- Se agregaron métricas base por secuencia y agregadas por proyecto: palabras originales, post-depuración, post-edición final, recortes IA y ajustes manuales posteriores.
- Se creó un sistema persistente de `prompt versions` para depuración con activación explícita, fallback al prompt integrado y enlace automático del prompt custom del workspace a una versión auditable.
- Se añadió el slide 5 del shell con tres paneles: resumen de análisis profundo, comparativa por secuencia y gestión de `prompt versions`.
- La UI puede guardar una versión nueva del prompt, activarla, volver al prompt base integrado y cargar una propuesta generada por IA dentro del editor de versiones.

#### Validación

- `python manage.py makemigrations --check --dry-run` sin cambios pendientes.
- `python manage.py check` sin issues.
- `npm run build` correcto tras integrar el slide 5.
- Validación HTTP local de endpoints nuevos:
	- `GET /api/editor/projects/25/deep-analysis/` -> `200`
	- `GET /api/core/depuration-prompt-versions/` -> `200`
	- `POST /api/core/depuration-prompt-versions/` -> `201`
	- `POST /api/core/depuration-prompt-versions/<id>/activate/` -> `200`
	- `POST /api/editor/projects/25/deep-analysis/` -> `200`

#### Observaciones

- El proyecto 25 ya valida el camino exitoso completo del análisis profundo gracias a una corrida persistida cuya comparativa se reconstruye desde `request_payload.user_payload.sequence_words`; con eso el `POST` devuelve reporte editorial y propuesta de prompt aunque la secuencia no estuviera marcada como `depurationSession.applied`.

### 2026-03-29 (Nueva fase context-map persistente)

#### Trabajo realizado

- Se agregó una fase nueva `context-map` al pipeline editorial antes del `segment-map`.
- Esta fase construye un mapa descriptivo persistente con dos capas reutilizables: `observations` cronológicas y `contexts` más amplios.
- El `context-map` queda guardado dentro del `structured_response` de `AISuggestion` y se expone en el bootstrap del proyecto junto con `conversation_map` y `segment_map`.
- Se añadió soporte OpenAI para esta fase con un prompt separado, enfocado en describir el flujo del transcript sin proponer todavía secuencias.
- También se añadió un fallback local heurístico para generar `context_map` aun cuando no haya OpenAI disponible.
- El `segment-map` ahora consume explícitamente `context_map` como entrada adicional, de modo que la segmentación editorial ya no parte solo del transcript crudo.
- Se simuló el método sobre el caso actual de Jimny y el fallback local ya produjo contextos reutilizables como `numero base y comunidad`, `senaletica y caravana`, `taller movil y auxilio`, etc.

#### Decisiones tomadas

- La lectura descriptiva del transcript merece su propia fase persistente, separada de la fase que decide macrosegmentos editoriales.
- `context_map` debe ser reutilizable para operaciones futuras como descripciones, segmentación, depuración y regeneración de sugeridas.
- La fuente principal de verdad para estos contextos debe quedar en backend, no solo en almacenamiento local del frontend.

#### Proximo paso

- Ejecutar una corrida editorial completa usando esta nueva fase y verificar si los contextos persistidos mejoran la calidad de la división posterior en sugeridas.

### 2026-03-27 (Limpieza sugeridas 34-58)

#### Trabajo realizado

- Se recortó manualmente el lote editorial activo del proyecto actual para conservar solo las sugeridas `#1` a `#33` en backend.
- La carga del proyecto en frontend ahora toma el lote activo de backend como fuente de verdad cuando existe, en lugar de mezclarlo con sugeridas viejas persistidas localmente.
- Si un proyecto ya trae sugeridas activas desde backend, el almacenamiento local de sugeridas para ese proyecto se limpia para evitar que reaparezcan tarjetas heredadas o duplicadas al recargar.

#### Decisiones tomadas

- Cuando existe un lote activo guardado en backend, ese lote manda sobre cualquier resto local previo del mismo proyecto.
- La persistencia local sigue siendo util para sugeridas temporales no guardadas en backend, pero no debe contaminar una corrida editorial ya persistida.

#### Proximo paso

- Recargar el proyecto y confirmar visualmente que la barra de `Secuencias sugeridas` quedó en `33` elementos sin arrastre de tarjetas viejas `#34+`.

### 2025-07 — Pipeline temático (Fase 1+2+3b)

#### Trabajo realizado

**Backend (100% completo):**
- `services.py`: `_detect_clean_speech_ranges()` — filtro local de ruido (ventana deslizante)
- `services.py`: `_build_thematic_segment_payload()` — mapea rangos limpios a segmentos para OpenAI
- `services.py`: `suggest_thematic_sequences(project, settings_obj)` — facade completa Fase 1+2; devuelve `[{sequence_id, label, description, word_ids}]`
- `services.py`: `depurate_sequence(project, settings_obj, word_ids, ...)` — Fase 3b; devuelve `{words_to_deactivate, display_mode_blocks, depuration_summary, engagement_notes}`
- `core/models.py`: campo `depuration_prompt` en `WorkspaceSettings`
- `core/migrations/0007_add_depuration_prompt.py`: migración aplicada ✅
- `editor/views.py`: `ProjectSuggestThematicSequencesView` y `ProjectDepurateSequenceView`
- `editor/urls.py`: rutas `suggest-thematic-sequences/` y `depurate-sequence/`

**Frontend (100% completo):**
- `DashboardPage.jsx`: estados `thematicBusy`, `depurationBusy`, `depurationResult`
- `DashboardPage.jsx`: `handleSuggestThematicSequences()` — llama al endpoint, convierte word_ids en sequence drafts con `createSequenceDraftFromWords`
- `DashboardPage.jsx`: `handleDepurateSequence()` — llama al endpoint, aplica `inactiveWordIds` + `clipDisplayModes`, muestra resultado
- `DashboardPage.jsx`: botón "Sugerir temático" en toolbar del script
- `DashboardPage.jsx`: botón "Depurar" en toolbar de secuencia
- `DashboardPage.jsx`: panel `depuration-result-panel` con resumen y momentos destacados
- `app.css`: estilos para `.depuration-result-panel` y subelementos

#### Decisiones tomadas

- El nuevo pipeline NO reemplaza al existente ("Sugerir"), ambos coexisten
- "Sugerir temático" añade secuencias nuevas sin tocar las existentes
- "Depurar" opera solo sobre la secuencia activa
- Los `display_mode_blocks` se mapean via `derivedBlocksRef.current[].words[last].sourceWordId`
- Contexto para depuración: 30 palabras antes y después del rango de la secuencia

#### Próximo paso

- Prueba manual end-to-end: abrir proyecto con transcript → Sugerir temático → revisar secuencias → Depurar
- Opcional: añadir `depuration_prompt` al panel de Settings (SettingsPage.jsx)
- Opcional: feedback loop para mejorar el prompt de depuración con el tiempo

---


### Trabajo realizado

- El pipeline de `Secuencias sugeridas` incorporó un pase explícito de `conversation-map` antes del armado de macrosegmentos para distinguir tramos conversacionales de zonas muertas, relleno o deriva temática.
- El descubrimiento de rangos dejó de operar sobre todo el scope bruto: ahora la generación de candidatos se apoya primero en los segmentos clasificados como conversacionales y usa ese mismo mapa también en el fallback heurístico.
- El pase de `judge` editorial se reforzó con contexto global del guion: cuando el alcance total es razonable revisa todo el scope y, si no lo es, recibe igualmente un outline amplio para no juzgar cada rango como isla.
- El payload persistido de sugeridas ahora guarda también `conversation_map`, de modo que frontend y futuras herramientas de depuración puedan inspeccionar cómo se separó conversación útil frente a relleno.

### Decisiones tomadas

- El flujo editorial correcto no es `ventana -> candidato -> juicio`, sino `mapa conversacional -> rango útil -> candidato -> juicio global`.
- Si un rango termina siendo fallback, la degradación debe seguir respetando el mapa conversacional siempre que exista, para evitar volver al comportamiento ciego basado en longitud.

### Bloqueos o riesgos

- La clasificación conversacional actual mezcla heurística local con el resto del pipeline; mejora mucho el punto de partida, pero seguirá necesitando validación con guiones reales para ajustar falsos positivos o falsos descartes.

### Próximo paso

- Ejecutar un scan editorial real sobre un proyecto ya transcrito y revisar si el agente reduce zonas marcadas como `fallback heurístico` y si los nuevos rangos se alinean mejor con conversaciones completas.

### Trabajo realizado

- Se corrigió el borrado de `Secuencias sugeridas` en frontend para que `Borrar todas` y `Borrar seleccionada` limpien la UI de forma consistente aunque el `bootstrap` refrescado no llegue perfectamente alineado con el estado visible.
- Se rehizo el cálculo del ajustador de altura de `Script principal`: ahora el drag se basa en `altura inicial + delta del puntero`, evitando que el panel quede aparente o realmente fijado a una altura rígida.

### Decisiones tomadas

- En acciones destructivas del editor conviene sanear localmente el bloque afectado además de confiar en la respuesta de backend, para que la UI no quede mostrando sugeridas fantasma.
- Los resizers verticales del layout deben usar un modelo incremental simple (`startY`, `initialHeight`) en vez de offsets relativos a contenedores scrolleables.

### Bloqueos o riesgos

- Si en el futuro se cambia la estructura scrollable del editor, habrá que mantener este enfoque incremental en el resizer para no reintroducir drift o topes falsos.

### Próximo paso

- Probar manualmente el borrado total, el borrado individual y el divisor del script en un proyecto con varias sugeridas y scroll vertical activo.

### Trabajo realizado

- El scan de `Secuencias sugeridas` ahora inyecta en el agente editorial los objetivos guardados en `Settings > Notas operativas`, para que discovery y juicio no trabajen ciegos respecto al criterio del workspace.
- Cada fallback del pipeline editorial dejó de ser opaco: ahora se persisten `candidate_source`, `judge_source`, `visual_source` y un `*_source_detail` explicando si hubo falta de API key, error de OpenAI o caida por alcance insuficiente.
- La UI de tarjetas y revisión de sugeridas ahora muestra una nota de `Pipeline` y pills de procedencia (`Discovery IA`, `Juicio fallback`, `Visual heuristico`, etc.) para depurar por qué salió cada rango.

### Decisiones tomadas

- Un fallback no debe esconderse bajo una explicación editorial genérica; la procedencia técnica del rango tiene que quedar visible para distinguir calidad del hallazgo contra limitaciones operativas del pipeline.
- Los objetivos editoriales persistidos del workspace deben formar parte del prompt de sugerencias, no quedarse como una nota administrativa separada del agente.

### Bloqueos o riesgos

- La mejora actual hace visible la causa del fallback y mete los objetivos del workspace en discovery/judge, pero si la key o el modelo fallan repetidamente seguirá existiendo dependencia de la heurística local hasta resolver la causa externa.

### Próximo paso

- Ejecutar un scan editorial real y comprobar si baja el volumen de tarjetas con fallback, y si las que permanezcan ahora muestran una causa concreta accionable.

### Trabajo realizado

- Se separó el flujo de `abrir sugerida` del flujo de ajustes con IA: ahora abrir una sugerida ya no relanza una reinterpretación automática del rango, sino que fija ese rango y monta la revisión con el mismo armado base del editor para texto, clips y preview.
- El pase por prompt quedó redefinido como edición asistida sobre operaciones reales del programa: activar o desactivar palabras, reordenar bloques, ajustar cortes y rehacer el plan visual sin inventar material fuera del alcance actual.
- Se reemplazó el scan de `Secuencias sugeridas` basado en ventanas temporales por una heurística guiada por contenido: ahora el backend puntúa segmentos por hook, densidad, payoff, autosuficiencia y señales de relleno antes de decidir qué rango merece existir como sugerida.
- Los rangos detectados ya no nacen de una plantilla rígida `20/40/20`; primero se eligen núcleos editoriales valiosos y después se expanden con contexto contiguo hasta una banda de duración razonable según el propio contenido.
- El filtro de solapes dejó de favorecer simplemente el rango más temprano en el tiempo y ahora prioriza el candidato con mejor score editorial cuando dos sugeridas compiten por el mismo tramo.
- Cada sugerida nueva ahora guarda metadata adicional para auditoría editorial: `why_it_matters`, `format_fit` y `content_score`.
- Se añadió un endpoint para vaciar las sugeridas activas del proyecto desde UI.
- La pantalla principal sumó un botón `Borrar sugeridas` dentro del módulo de `Secuencias sugeridas`.
- Se borraron las sugeridas guardadas actualmente en la base de desarrollo: 8 lotes de `AISuggestion` eliminados.
- La UI de `Secuencias sugeridas` ahora muestra de forma explícita por qué salió cada rango, qué texto núcleo disparó el hallazgo y si conviene más para vertical, horizontal o ambos formatos.

### 2026-03-26

#### Trabajo realizado

- Se rehizo el drag del divisor del `Script principal` para usar `pointer events` en una sola ruta (`onPointerDown` + listeners globales `pointermove/pointerup/pointercancel`).
- Se eliminó el `id` fijo del divisor de cortina para evitar colisiones de `id` duplicado si el editor llega a montarse más de una vez en desarrollo.

#### Decisiones tomadas

- El resizer de script debe usar una sola capa de eventos (pointer) para evitar desincronización entre mouse y touch.
- El divisor no necesita `id` fijo; clase + ARIA son suficientes y más seguros frente a montajes duplicados.

#### Próximo paso

- Validar manualmente en navegador que la cortina responde con arrastre continuo (mouse y touch) y que persiste la altura al soltar.
- El modal de revisión editorial también expone esa justificación al inicio de cada fragmento para que la depuración no empiece a ciegas.
- El módulo `depurar` ahora enseña en paralelo dos previews escalados del mismo bloque editado: uno horizontal `16:9` y otro vertical `9:16`, para comparar el encuadre real sin cambiar de modo manualmente.
- Ese doble preview dejó de ser un espejo exacto: ahora cada lado resuelve su propio `display mode` por aspecto y aplica un encuadre por formato más coherente con la preferencia editorial vertical u horizontal.
- Las sugeridas y los candidatos en revisión ahora marcan con más fuerza la prioridad de formato (`vertical`, `horizontal` o `ambos`) usando banner y acento visual en la tarjeta.
- Cada sugerida normalizada ahora guarda también un `aspect_display_plan` persistido con mini plan visual para `landscape` y `portrait`, incluyendo `mode`, `crop_preset` y razón por bloque.
- Al aprobar una sugerida, ese plan ya hidrata `clipDisplayModes` y `clipDisplayAdjustments` por aspecto dentro de la secuencia creada, en vez de recalcularse solo desde heurísticas del review.
- Se corrigió la construcción de clips dentro de `depurar`: ya no se parte el rango con heurísticas agresivas por palabras conservadas, sino reutilizando el mismo modelo del editor de secuencia (`createSequenceDraftFromClips` + `deriveSequenceBlocks` con `customSplitPoints`, `customStartPoints` y `customEndPoints`).
- Con eso, si la IA quita una parte intermedia de una oración, el preview crea dos clips; si sólo recorta el inicio o el final, mantiene un solo clip con borde exacto, igual que en el timeline real.
- La barra de `Secuencias sugeridas` ahora permite seleccionar una tarjeta concreta con click y el botón de borrar actúa sobre esa seleccionada; si no hay selección, borra todas las sugeridas del lote activo.
- Se ejecutó una limpieza directa de las sugeridas almacenadas en la base de desarrollo para dejar el proyecto sin lotes editoriales guardados.
- Se validó todo con `python manage.py check` en backend y `npm run build` en frontend.

### Decisiones tomadas

- Un rango sugerido no debe nacer de una duración objetivo sino de una unidad de contenido que realmente tenga valor editorial propio.
- La duración ahora debe ser una consecuencia del contenido y del contexto mínimo útil, no un objetivo impuesto antes de evaluar el valor del tramo.
- Si el sistema propone dos rangos que se pisan, debe conservar el más fuerte editorialmente y no simplemente el primero cronológicamente.
- La explicación editorial no debe quedar escondida como metadata cruda; tiene que verse en la tarjeta y dentro del review para que el usuario entienda la apuesta antes de abrir o aprobar el corte.
- En `depurar`, comparar horizontal y vertical lado a lado es más útil que alternar un único preview activo, porque la decisión editorial de formato se toma más rápido viendo ambos encuadres sobre el mismo instante del corte.
- La recomendación de formato debe leerse de inmediato en la UI; no basta con una metadata pequeña si el usuario está cribando rápido qué sugerida vale abrir primero.
- Si el sistema ya sabe que una sugerida funciona distinto en horizontal y vertical, esa decisión tiene que persistirse con la sugerida para que el review y la aprobación trabajen sobre el mismo plan visual.
- `Depurar` no debe tener una interpretación paralela del transcript respecto al timeline: ambos deben leer los mismos objetos de clip y las mismas reglas de corte exacto para evitar previews engañosos.
- El borrado de sugeridas no debe ser ciego ni ambiguo: la UI tiene que permitir apuntar a una sugerida concreta antes de eliminarla, y seguir conservando una salida rápida para vaciar el lote completo.

### Bloqueos o riesgos

- La heurística nueva mejora mucho el fallback local, pero sigue siendo una heurística; todavía convendrá contrastarla con criterio humano y, más adelante, con una fase de scoring visual más explícita para distinguir mejor qué va a vertical, horizontal o ambos.

### Próximo paso

- Generar un lote nuevo de sugeridas sobre un proyecto real y revisar si los rangos detectados ya se sienten elegidos por valor de contenido en vez de por longitud arbitraria.

- Riesgo o bloqueo

### Próximo paso

- Acción inmediata

---

## 2026-03-22

## 2026-03-23

### Trabajo realizado

- Se ajustó el botón `Sugerir` para que deje de parecer inerte cuando el proyecto todavía no tiene transcript usable: ahora se bloquea si no hay suficientes palabras temporizadas y muestra un mensaje claro en `editor` explicando que primero hay que transcribir.
- Se corrigió la persistencia del draft del proyecto para reducir los bloqueos de SQLite al guardar solo `editor_draft`, evitando un write redundante del serializer que estaba disparando `database is locked` con frecuencia en desarrollo.
- El draft local ahora guarda también la selección actual del script fuente (`selectedSourceIds`, ancla y foco), así que refrescar la página ya no obliga a volver a seleccionar el rango antes de pulsar `Sugerir`.
- Se replanteó la pantalla de `depurar` para que deje de sentirse como un panel técnico ambiguo: ahora prioriza a la izquierda la transcripción completa del rango y el texto deshabilitado por IA, y a la derecha el preview del resultado editado, el prompt de cambios y el costo disponible.
- El preview de depuración dejó de exponer los controles nativos del video fuente completo; ahora se auto-posiciona sobre el primer bloque del corte editado y se reproduce con controles propios del montaje secuencial del rango.

### Decisiones tomadas

- El flujo editorial no debe arrancar un run que va a fallar casi de inmediato por falta de transcript; en ese caso conviene fallar antes, en frontend, con una explicación visible.
- La selección de texto usada como foco editorial debe vivir en el draft igual que las secuencias, porque forma parte del estado de trabajo inmediato del editor.
- En la fase `depurar`, el usuario no necesita ver primero una cola larga de módulos técnicos; lo central es entender qué texto quedó, qué texto se deshabilitó y cómo se ve el resultado editado con audio.

### Bloqueos o riesgos

- El bloqueo actual usa disponibilidad de `wordTimings` en frontend como proxy de transcript listo; si en el futuro cambia la carga de timings o se admite otro origen de sugerencias, habrá que revisar esa condición.
- El costo mostrado en la depuración sigue siendo acumulado del proyecto, no de esa corrida específica; si luego se necesita costo exacto por `refine/adjust`, habrá que instrumentarlo en backend.

### Próximo paso

- Probar manualmente el caso de un proyecto sin transcript y confirmar que el botón queda claramente deshabilitado y que el usuario entiende la acción requerida antes de usar `Sugerir`.

---

## 2026-03-24

### Trabajo realizado

- Se revisó el comportamiento real del scan editorial contra el transcript y las sugeridas guardadas, confirmando que la mayor pérdida de cobertura ocurría después de detectar las conversaciones: el pipeline seguía comprimiendo el inventario final con un límite derivado de `target_count`.
- El backend editorial quedó reorientado para priorizar `de qué se está hablando` en cada bloque y conservar un inventario amplio de conversaciones y subtemas detectados, en vez de sesgar el discovery hacia unas pocas piezas `short-form` destacadas.
- `segment-map`, `candidate-pass` y `judge-pass` ahora instruyen explícitamente a preservar cobertura conversacional, separar subtemas cuando tengan cierre propio y evitar recortar contexto útil solo para hacer piezas más cortas.
- La cola final de sugeridas ya no vuelve a comprimirse con `target_count * 2`; ahora toma como referencia la cantidad real de macrosegmentos detectados, respetando el inventario conversacional disponible.
- Se añadió una capa local de subdivisión temática sobre macrosegmentos largos: cuando el backend detecta una ruptura léxica clara entre dos mitades del bloque, lo divide antes del `candidate-pass` y marca esa frontera como guía estructural para la IA.

### Decisiones tomadas

- `target_count` pasa a ser una densidad inicial de exploración, no un cupo rígido que elimine conversaciones válidas detectadas por el propio pipeline.
- La prioridad editorial base deja de ser `clip corto de alto impacto` y pasa a ser `cobertura útil de conversaciones y subtemas`, dejando la compresión fina para depuración posterior.
- La estructura temática básica de un bloque no debe depender solo de OpenAI; el backend puede detectar rupturas locales y entregar a la IA una mejor unidad de trabajo antes del juicio editorial.

### Bloqueos o riesgos

- Al conservar más conversaciones y subtemas, la revisión humana puede recibir más sugeridas por corrida; habrá que validar si el volumen sigue siendo cómodo en UI o si conviene sumar más herramientas de filtrado.

### Próximo paso

- Ejecutar un scan editorial nuevo sobre el proyecto real y medir si la cobertura por palabras retenidas se acerca mucho más a la cobertura conversacional detectada.

### Trabajo realizado

- El header principal del editor ahora muestra un contador diario compacto de uso OpenAI para el proyecto activo, con gasto del día, tokens `IN`, tokens `OUT`, tokens totales e iteraciones totales.
- El contador se alimenta desde `openai-trace` y se refresca automáticamente al terminar una corrida del agente editorial para que no quede desactualizado tras `scan`, `refine` o `adjust`.
- Ese resumen visible en header se amplió a dos conjuntos compactos: `Proyecto` para el acumulado total del proyecto y `Mes` para el acumulado del mes actual dentro de ese proyecto.
- Se añadió soporte opcional para consultar costos oficiales de OpenAI vía `GET /organization/costs` usando `OPENAI_ADMIN_API_KEY`; esos montos ya aparecen en `Status` como referencia oficial separada del estimado local.
- La UI dejó de tratar un `0.0000` como si siempre fuera un costo válido: ahora expone explícitamente si la telemetría de costo está en estado `full`, `partial` o `blind`, con advertencias cuando faltan precios locales o `OPENAI_ADMIN_API_KEY`.

### Decisiones tomadas

- La telemetría económica útil para operar el agente no debe quedar escondida en `Status`; un resumen técnico pequeño y siempre visible en el header reduce fricción al evaluar costo por uso.
- El costo oficial de OpenAI no debe mezclarse con el costo por proyecto local si no existe una correspondencia 1:1 entre proyecto local y proyecto de OpenAI; por eso se expone como costo oficial de organización aparte del estimado local.
- Cuando la app no puede saber dinero real con suficiente confianza, debe decirlo de forma visible en vez de dejar al usuario interpretar ceros ambiguos.

### Bloqueos o riesgos

- El contador diario actual está agregado por proyecto y por día calendario local; si más adelante hace falta un corte por corrida o por fase en tiempo real dentro del header, habrá que extender el backend con breakdown adicional por `operation_name`.

### Próximo paso

- Verificar visualmente que el contador del header siga siendo legible con proyectos largos y que actualice sus números tras una corrida completa sin recargar la página.

### Trabajo realizado

- Se detectó que el problema pendiente ya no estaba tanto en el rango externo, sino en el pruning interno: algunos candidatos conservaban un tramo temporal amplio pero desactivaban demasiadas palabras dentro de ese mismo tramo, generando previews con mucho texto rosa y poca cobertura real.
- `refine` dejó de aceptar silenciosamente ese adelgazamiento interno: ahora, salvo que exista una instrucción explícita de recorte, bloquea candidatos que mantengan casi la misma duración pero vacíen demasiado el contenido hablado del seed.
- La misma protección se extendió a `candidate-pass` y `judge-pass`, de modo que el scan y la depuración no guarden ni reafirmen rangos con huecos internos excesivos aunque la duración total todavía parezca razonable.

### Decisiones tomadas

- El criterio de sobre-recorte no puede medirse solo por duración; también hay que vigilar cuánta palabra hablada queda activa dentro del propio tramo temporal elegido.
- Si el usuario no pidió explícitamente un recorte fino, `depurar` debe comportarse de forma conservadora y preservar cobertura conversacional amplia antes que producir una versión demasiado apretada.

### Bloqueos o riesgos

- La nueva barrera evita mejor los rangos huecos, pero todavía hará falta validar con proyectos reales si el umbral de cobertura interna queda bien calibrado o si conviene afinarlo por tipo de contenido.

### Próximo paso

- Ejecutar de nuevo un scan o abrir otra sugerida problemática y comprobar si el review deja de mostrar grandes masas de palabras deshabilitadas dentro de un rango que, en tiempo, ya parecía correcto.

### Trabajo realizado

- Se identificó la causa raíz de la baja cobertura editorial: el scan procesaba como máximo 6 macrosegmentos candidatos aunque el `conversation_map` hubiera detectado muchas más conversaciones coherentes dentro del alcance.
- Se amplió el techo de `segment-map` y `candidate-pass` para que el pipeline pueda recorrer muchos más bloques conversacionales antes de juzgar y guardar sugeridas.
- La cola final de sugeridas dejó de limitarse al `target_count` bruto del scan; ahora puede guardar un inventario bastante más amplio para revisión humana antes de la depuración fina.
- Se añadió una auditoría persistida por scan (`audit`) con métricas de cobertura: palabras dentro del alcance, palabras conversacionales detectadas, palabras cubiertas por las sugeridas guardadas, número de rangos conversacionales detectados, macrosegmentos generados y ratios de cobertura.
- La pantalla `Status` y la barra del `Agente editorial` ahora exponen esa auditoría para medir explícitamente cuánto del hablado coherente está quedando cubierto por el pipeline.
- Se extendió el logging de OpenAI para empezar a registrar también interacciones editoriales con costo estimado por tokens, además de la transcripción por audio.
- Se creó y aplicó la migración `editor.0008_openai_log_editorial_fields` para soportar esos campos nuevos de logging.

### Decisiones tomadas

- El objetivo del scan no debe ser sacar pocas sugeridas finales muy agresivas, sino construir primero un inventario suficientemente amplio de conversación coherente para revisión editorial humana.
- La métrica correcta para juzgar calidad del discovery no es solo cuántas sugeridas finales aparecen, sino qué porcentaje del hablado coherente fue detectado, segmentado y preservado para revisión.
- El límite `target_count` debe entenderse como estimación inicial de densidad editorial, no como tope rígido de cobertura conversacional.

### Bloqueos o riesgos

- Aunque el techo de candidatos ya no es tan restrictivo, todavía hará falta validar con proyectos reales si el nuevo balance entre amplitud de cobertura y costo OpenAI se mantiene razonable en scopes muy largos.

### Próximo paso

- Ejecutar un scan nuevo sobre un proyecto real y contrastar la auditoría (`rangos detectados -> macrosegmentos -> sugeridas guardadas`) contra la lectura humana del transcript para ajustar el siguiente cuello de botella si todavía faltan conversaciones.

### Trabajo realizado

- La guía visual dejó de depender de unos pocos frames sueltos y ahora pasa a un `contact sheet` temporal de 64 frames sobre un rango extendido antes y después de la secuencia sugerida.
- Cada frame del `sheet` ahora conserva posición temporal explícita (`time_ms`), contexto relativo (`before`, `inside`, `after`), rango extendido usado para la guía y separación entre muestras, para que la depuración no vea solo imágenes sino también su ubicación temporal.
- La guía visual empezó a guardar también `reference_frames`, un subconjunto reutilizable de frames pensados como semillas para futuros candidatos de B-roll.
- Se añadió poda automática para la caché de thumbnails del timeline y para los `visual_guides` por video, manteniendo solo los lotes más recientes en disco en vez de acumular indefinidamente cada ventana generada.
- La UI de depuración ahora muestra el `sheet` completo, los `reference_frames` y la cobertura temporal del muestreo visual, además del resumen que genera la API.

- Se añadió una cuarta pasada editorial `visual-guide-pass` después del juicio del rango. Ahora cada sugerida final genera una guía visual reutilizable a partir de frames representativos del rango antes de quedar guardada.
- Esa guía visual ya no es solo un manifiesto técnico de thumbnails: cuando hay API disponible, el backend pide una revisión visual estructurada y persiste resumen de escena, participantes visibles, objetos, acciones, notas de continuidad y oportunidades de B-roll interno.
- Cuando no hay API o no se puede revisar visualmente, el sistema igual deja una guía visual provisional basada en frames y hints de diarización (`speaker_label`) para no perder el hilo entre sugerida, depuración y corrección final.
- Cada candidata ahora guarda también `final_prompt_context`, una memoria acumulada con ancla conversacional, ancla visual, puntos que no conviene perder y chequeos finales para el último prompt de correcciones.
- `Depurar` y `ajustar` actualizan esa guía visual después de cada nuevo pase, de modo que el rango abierto siempre mantiene sincronizadas su comprensión textual, su juicio editorial y su memoria visual.
- La UI ahora expone esa guía visual revisada en las tarjetas de sugeridas y en la revisión principal del agente, incluyendo una tira de frames representativos y el bloque `Memoria acumulada para el prompt final`.

- El `scan` inicial de `Secuencias sugeridas` dejó de depender de una sola heurística plana y ahora corre un pipeline de tres pasadas: `segment-map`, `candidate-pass` y `judge-pass`.
- La primera pasada ahora intenta separar el transcript en macrosegmentos editoriales según transiciones de contenido, cambios de contexto y bloques conversacionales antes de proponer cortes concretos.
- La segunda pasada propone un rango candidato por cada bloque detectado, priorizando que el corte nazca dentro de una unidad temática y no solo por score temporal o duración fija.
- La tercera pasada añade un juicio editorial explícito por candidato para verificar si el rango se entiende solo, si necesita más contexto antes o después, si mezcla tópicos y si conviene conservar o colapsar pausas internas.
- El backend empezó a persistir metadata nueva por sugerida para futuras capas de UI y revisión: `topic_label`, `segment_type`, `judge_summary`, `segment_group_id` y `editorial_judgement`.
- Ese mismo juicio editorial ya no queda solo en el scan: las fases de `depurar` y `ajustar` ahora vuelven a pasar la sugerida por una verificación de completitud para evitar que el refine deje un corte técnicamente limpio pero editorialmente incompleto.
- Además del rango, cada sugerida ahora puede guardar comprensión de conversación reutilizable (`conversation_summary`, `coverage_explanation`, `topic_transitions` y `range_precision`) para que `depurar` no vuelva a empezar desde cero y use el mismo entendimiento editorial del bloque detectado.
- La UI de `Secuencias sugeridas`, la tarjeta principal de `depurar` y la lista de otros fragmentos ahora muestran esa comprensión conversacional: qué conversación cubre el rango, qué transición detectó, si el sistema lo considera preciso o todavía amplio, y si cree que el corte ya se entiende solo.
- El modal del agente editorial ahora muestra también un `Mapa técnico de bloques y transiciones`, resaltando qué macrosegmentos detectó, cuáles todavía parecen divisibles y cuál de ellos cruza el rango actualmente en revisión.
- Las tarjetas y reviews ahora marcan explícitamente cuando un rango todavía parece `divisible` en subrangos más precisos, en vez de presentarlo como definitivo si el propio juicio editorial detectó cambio de tópico o bloque demasiado amplio.
- El prompt de `depurar` se reforzó para reutilizar `conversation_summary`, `coverage_explanation` y `topic_transitions` como memoria editorial del bloque original; además, el juicio posterior ahora puede declarar si el refine preservó o cambió el entendimiento conversacional inicial.
- Se mantuvo compatibilidad con el flujo actual: si la fase iterativa no consigue suficientes candidatos o no hay API, el sistema cae al fallback heurístico existente en lugar de dejar el scan vacío.
- Se validó el cambio con `python manage.py check`.

### Decisiones tomadas

- El `sheet` visual no debe ser solo una imagen compuesta: cada frame necesita identidad temporal y contexto relativo para que la API pueda usarlo al depurar y más adelante al sugerir B-roll.
- Los assets visuales derivados sí se pueden cachear en disco, pero con poda por recencia para que el proyecto no crezca sin límite en el equipo local.

- La memoria visual no debe reconstruirse al final desde cero: conviene fijarla justo después de validar cada rango para que depuración y prompt final trabajen sobre la misma evidencia.
- Para controlar costo y complejidad, la primera implementación usa frames representativos del rango en lugar de un contact sheet denso único; la arquitectura ya deja listo el punto donde más adelante se puede sustituir por una hoja visual más rica si hace falta.

- La detección inicial ya no debe preguntar primero "qué rango corto saco", sino "qué bloques de sentido hay en esta cobertura".
- El estado editorial entre pasadas debe ser explícito y persistido por el backend; no conviene depender de una memoria implícita de conversación entre llamadas al API.
- El juicio de completitud pertenece al `scan` inicial, no solo a la fase de `depurar`, porque mejora qué sugeridas vale la pena guardar desde el principio.

### Bloqueos o riesgos

- La versión nueva puede consumir más llamadas al API que el scan heurístico puro, así que todavía conviene vigilar costo y latencia en proyectos largos; por eso se dejaron límites y fallback local.

### Próximo paso

- Validar sobre un proyecto real si el `sheet` de 64 frames mejora la depuración y decidir el siguiente paso entre: selección automática de `reference_frames` más rica o primer generador de `broll_candidates` a partir de esos frames.

- Probar sobre un proyecto real si la guía visual revisada mejora decisiones de depuración y definir si el siguiente salto debe ser un `contact sheet` más denso o un pase específico de sugerencias de B-roll.

- Exponer en la UI del agente y de `Secuencias sugeridas` la metadata nueva del `judge-pass` para que el usuario vea por qué un rango quedó marcado como autosuficiente, expandido o recortado.

### Trabajo realizado

- El backend del agente editorial dejó de depender de una sola llamada grande para todo el alcance: ahora hace primero un `scan` global de rangos candidatos y luego refina cada fragmento secuencialmente. El progreso expone `active_fragment_id`, y el frontend ya sigue automáticamente el fragmento que el run está trabajando durante el procesamiento.
- Se definió la estrategia de uso de API para el futuro agente editorial progresivo: primero hacer un `scan` global barato para detectar rangos candidatos y después trabajar cada fragmento de forma secuencial y visible con IA. Se descartó tanto la llamada única gigante para resolver todo el alcance como el enfoque de una llamada remota por cada microtramo del transcript.
- El overlay del agente dio el primer paso real hacia el modelo de `fragmento activo`: ahora muestra una tira de fases editoriales, un mapa vivo del texto en trabajo, diff `Original` vs `Depurado`, una fase explícita de `Orden narrativo`, otra de `Decision visual` por bloque y mantiene el preview provisional dentro del mismo flujo, en lugar de centrar la UX en una simple lista de candidatos.
- Se acordó una política visual explícita para la futura fase `fit/fill/split` del agente editorial: `fit` queda como baseline conservador, `fill` solo se usa cuando el sujeto o foco visual central realmente sostiene pantalla llena, y `split` se reserva para bloques explicativos donde imagen y lectura deban convivir. La decisión futura se apoyará en texto depurado + bloques narrativos + thumbnails del rango activo, no en elecciones azarosas del prompt.
- Se hizo mas honesto el overlay del agente durante `discovery/rewrite`: la lista de `Candidatos visibles` y el panel de `Revision de fragmento` ahora se alimentan de la misma fuente, y los candidatos que todavia no traen estructura suficiente quedan marcados como `Aun armando` en vez de ofrecer un `Revisar este` que no podia abrir nada.
- Se corrigió un error de desarrollo en la carga del waveform maestro del timeline: el frontend estaba intentando leer `http://127.0.0.1:8000/media/...` directamente desde `http://localhost:5173`, y ahora normaliza esos assets locales para pedirlos vía proxy de Vite en `/media`, evitando el bloqueo CORS durante la edición.
- Se separó el estado de edición fina entre `minimizada` y `cerrada por volver al workspace superior`: al reexpandir el chrome superior o el `Script principal`, el workbench inferior desaparece por completo para que Projects + Script recuperen toda el área de trabajo.
- Al elegir una secuencia nueva o pulsar `Abrir edición fina`, el workbench vuelve a abrirse normalmente con transcript, preview y timeline.
- El modo superior ahora también oculta el `Preview selección` dentro de `Script principal`, dejando visible solo el transcript fuente, la barra de secuencias creadas y el arranque del agente editorial desde esa misma zona.
- `Sugerir` ahora prioriza la selección actual del `Script principal` como foco editorial; si existe un rango seleccionado, el backend ya no cae por defecto a la secuencia activa para definir alcance y cantidad de candidatos.
- El overlay del agente dejó de autoaplicar todas las sugerencias al finalizar: ahora entra en una cola de revisión por fragmento, con preview secuencial del candidato y acciones de `Aprobar y agregar secuencia` o `Descartar y seguir` para avanzar manualmente por el texto descubierto.

### Decisiones tomadas

- Reabrir el área superior debía comportarse como un cierre implícito de la secuencia actual, porque minimizar no resolvía el problema de recuperar realmente la pantalla para volver a revisar el proyecto y el script fuente.
- Mientras el usuario está pensando en discovery y generación editorial, el preview lateral del script mete ruido visual; conviene reservarlo para cuando ya vuelva a abrir una secuencia concreta en el editor fino.
- El alcance visible del agente debe corresponder al gesto real del usuario: si selecciona un rango del script, la auditoría de palabras elegidas, desactivadas y reordenadas tiene que ocurrir sobre ese rango y no sobre una secuencia previa que quedó activa.
- Para que el agente se sienta como un proceso editorial supervisable, el final del flujo no debe ser un auto-commit silencioso: conviene presentar cada hallazgo como un fragmento revisable y dejar que la aprobación humana marque qué secuencias pasan al editor fino.

### Bloqueos o riesgos

- La secuencia activa se conserva como contexto aunque el workbench quede cerrado; si más adelante esto resulta confuso, quizá convenga distinguir visualmente entre `secuencia activa` y `secuencia abierta en edición`.

### Próximo paso

- Validar manualmente que, al expandir de nuevo el `Script principal` o el chrome superior, la zona inferior desaparezca y solo reaparezca al elegir secuencia o reabrir la edición fina.

---

### Trabajo realizado

- Se reencuadró el flujo del botón `Sugerir` para acercarlo a la lógica editorial esperada: al terminar el run, las sugerencias ya no dependen solo del modal efímero del agente sino que quedan persistidas en el proyecto como `Secuencias sugeridas` dentro del bootstrap del editor.
- La UI principal ahora muestra un módulo persistente `Secuencias sugeridas` justo debajo de `Secuencias creadas`, de modo que los rangos detectados siguen visibles y reabribles después de cerrar el modal o recargar el proyecto.
- Abrir una sugerida ya no exige relanzar todo el proceso: cada card guardada puede reabrir el modal del agente sobre esa sugerencia concreta para revisarla y depurarla.
- El modal del agente se simplificó para reducir ruido: la vista principal ahora prioriza `Qué encontró`, `Trabajo sobre el texto` y `Preview con audio`, mientras que `Orden narrativo` y `Decisión visual` quedaron detrás de un detalle opcional.
- El preview editorial dejó de reproducirse silenciado; el video del fragmento ahora se abre con controles y audio activo para que la revisión del rango tenga sentido narrativo real.

### Decisiones tomadas

- `Sugerir` debe comportarse como descubrimiento y guardado de rangos sugeridos, no como una apertura forzada del modal ni como un commit automático al editor fino.
- La revisión profunda de una sugerida debe arrancar desde una secuencia sugerida específica, porque esa interacción encaja mejor con el flujo deseado de `abrir rango -> depurar -> previsualizar -> ajustar -> aprobar`.
- La persistencia visual de las sugeridas en la pantalla principal es obligatoria para no perder trabajo entre pasadas del agente.

### Bloqueos o riesgos

- Aunque la UX ya quedó alineada a `rangos sugeridos -> abrir sugerida -> depurar`, el backend sigue refinando secuencialmente el lote completo dentro del mismo run; todavía falta separar formalmente el `scan` puro de rangos de una futura fase explícita de depuración por sugerida.
- Aún no existe la caja final de prompt para pedir cambios editoriales sobre una sugerida ya abierta; el modal quedó preparado para esa dirección, pero esa tercera pasada todavía no está implementada.

### Próximo paso

- Separar en backend el `scan` inicial de rangos de la depuración detallada por sugerida, para que `Sugerir` descubra y guarde primero, y la depuración profunda ocurra solo al abrir una sugerida concreta.

---

### Trabajo realizado

- Se completó la separación real del backend editorial en dos operaciones distintas. `generate-suggestion` ahora hace solo `scan` del alcance y persiste rangos sugeridos con ids estables; ya no refina todo el lote dentro del mismo run.
- Se añadió un endpoint nuevo de `refine-suggestion` para abrir una sugerida concreta y correr sobre ella la depuración editorial con IA, reutilizando el mismo sistema de polling y modal del agente.
- El bootstrap del proyecto ahora devuelve `Secuencias sugeridas` ya hidratadas para UI, con metadata legible de duración, palabras, cortes y ids persistentes para que puedan reabrirse después.
- El botón `Abrir y depurar` del frontend dejó de abrir datos guardados de forma estática y ahora dispara un run real de depuración sobre la sugerida elegida.

### Decisiones tomadas

- El scan debe ser una fase persistente y barata que produzca inventario de rangos antes de cualquier depuración profunda.
- La depuración editorial con IA debe ejecutarse solo sobre la sugerida que el usuario decide abrir, no sobre todo el lote detectado.

### Bloqueos o riesgos

- Falta todavía la tercera pasada explícita de feedback por prompt libre dentro del modal de depuración; por ahora el flujo ya separa `scan` y `depurar`, pero no incluye aún `ajustar con instrucciones del usuario`.

### Próximo paso

- Añadir la caja de instrucciones finales dentro del modal de depuración para que el usuario pida cambios editoriales sobre la sugerida ya refinada y reciba un nuevo preview antes de aprobarla.

---

### Trabajo realizado

- Se completó la tercera pasada del flujo editorial dentro del modal: ahora, después de abrir y depurar una sugerida, el usuario puede escribir instrucciones finales en una caja de prompt y pedir al agente que rehaga esa misma sugerida antes de aprobarla.
- El backend ya acepta `user_instructions` y el contexto de la sugerida actual para rehacer hook, depuración, orden narrativo y plan visual sobre el mismo rango abierto.
- Se añadió un endpoint dedicado `adjust-suggestion` que reutiliza el sistema de polling del agente editorial, de modo que el nuevo preview vuelve a aparecer dentro del mismo modal y no como un flujo aparte.
- El modal del agente ahora cubre las tres pasadas esperadas del workflow: `scan de rangos sugeridos`, `depuración de una sugerida concreta` y `ajuste final por instrucciones del usuario` antes de `Aprobar y agregar secuencia`.

### Decisiones tomadas

- Las instrucciones finales del usuario debían vivir dentro del mismo modal de depuración, no en una pantalla aparte, para que el ciclo `ver preview -> pedir cambio -> ver nuevo preview` se mantenga corto y auditable.
- La tercera pasada reutiliza el mismo rango y el mismo inventario de palabras ya detectado; el agente no vuelve a escanear todo el alcance, sino que rehace solo la sugerida activa.

### Bloqueos o riesgos

- Los ajustes finales todavía no se persisten de vuelta dentro de `Secuencias sugeridas` del bootstrap; viven en la revisión activa del modal hasta que el usuario aprueba la secuencia y la pasa a `Secuencias creadas`.

### Próximo paso

- Probar manualmente el ciclo completo `Sugerir -> Abrir y depurar -> Pedir cambios -> Reaprobar preview -> Aprobar y agregar secuencia` sobre un proyecto real para calibrar la calidad editorial del tercer pase.

### Trabajo realizado

- Se reemplazó el flujo legado de `generate-suggestion` por un contrato editorial estructurado: el backend ahora devuelve hasta 3 alternativas con `word_ids`, `break_after_word_ids`, `display_plan`, `sequence_name`, `hook_text`, `main_title`, `hashtags`, `publish_description` y duración objetivo.
- La sugerencia editorial ahora consume el draft actual del editor para tomar como foco la secuencia activa y respetar las correcciones inline del transcript (`transcriptWordOverrides`) al momento de pedirle alternativas a OpenAI.
- Se añadió contexto visual al prompt editorial usando thumbnails temporizados del video completo y, cuando existe, de la ventana enfocada de la secuencia activa, para que la IA relacione mejor lo dicho con lo que se ve.
- El botón `Sugerir` del frontend ahora convierte automáticamente la respuesta editorial en secuencias reales del editor, aplica metadata publicable por secuencia, restaura cortes con `breakAfterWordIds`, asigna modos visuales por bloque y reemplaza sugerencias AI previas sin tocar las secuencias manuales del usuario.
- Se validó el cambio completo con `python manage.py check` en backend y `npm run build` en frontend.
- La generación editorial ahora escala con la duración del alcance activo: si la secuencia/rango seleccionado es corto devuelve pocas piezas, y si el alcance crece estima la cantidad de highlights a partir de ventanas de ~90 segundos por pieza objetivo.
- Se añadió una garantía backend para evitar que dos secuencias AI sugeridas se superpongan temporalmente entre sí; si una propuesta pisa el inicio o final de otra, se descarta y se rellena con una alternativa no superpuesta.
- El botón `Sugerir` ahora dispara un agente editorial en segundo plano en lugar de una llamada ciega única: el frontend abre un módulo superpuesto de pantalla completa y va mostrando progreso, fases, candidatos visibles y consolidación final mientras el backend itera.
- La traza visible del agente muestra cómo se define el alcance, cómo se detectan candidatos por ventana, cuándo OpenAI reescribe o cuándo entra fallback, y qué secuencias terminan consolidadas al final sin solaparse.
- La ejecución del agente quedó desacoplada mediante polling con `run_id`, de modo que el usuario puede ver el avance en tiempo real y, al completarse, las secuencias se aplican automáticamente al editor.
- El overlay del agente ahora también conserva snapshots locales por iteración y muestra diffs visibles entre una pasada y la siguiente: renombres de secuencia, ajustes de título, aumento o reducción de palabras depuradas, cambios de estrategia visual y previews del texto seleccionado versus el descartado.
- Cada candidato visible del agente ahora incluye una mini-timeline comparando `Origen` versus `Final`, para que el usuario vea de un golpe qué bloques se movieron, cuál fue el orden original y qué tratamiento visual (`fill`, `fit`, `split`) quedó asociado a cada bloque.

- Se añadió una primera capa de metadata editorial persistente por secuencia: `mainTitle`, `hashtags` y `publishDescription` ya viajan en el draft serializado y quedan listas para que las respuestas de OpenAI se conviertan en secuencias publicables sin perder esa información al recargar.
- Se agregó un toggle de `Titulo principal` en el preview, paralelo al de subtítulos, para mostrar sobre el video el título principal asociado a la secuencia activa.
- Se corrigió la persistencia del estado de subtítulos del preview: `previewSubtitlesEnabled` y `previewSubtitleStyle` ahora forman parte del layout serializado, se restauran al reabrir proyecto y ya no se apagan al refrescar por vivir solo en estado local del componente.
- Se dejó la corrección final del waveform como un retraso visual de dibujo de 620 ms, aplicado sobre la posición renderizada de las barras y no sobre el muestreo interno, para alinear la forma de onda con el playhead según la lectura visual real del editor.
- Se corrigió la fuente de verdad de la waveform del timeline: en lugar de generarse desde el preview seekable recodificado, ahora se construye desde el audio WAV extraído del source original para evitar un offset fijo de codificación que se estaba percibiendo en torno a ~50 ms.
- Se ajustó la percepción de sincronía entre playhead, waveform y palabra activa en timeline: las barras del waveform dejaron de dibujarse centradas dentro de cada slot temporal y ahora arrancan alineadas al inicio real de su ventana; además, el playhead quedó centrado exactamente sobre su coordenada y la palabra activa pasó a resolverse con intervalo semiabierto para no quedarse un frame tarde en cambios limítrofes.
- Se corrigió la sincronización después de reordenar clips con drag: al soltar un bloque movido, el editor ahora recentra la selección, el playhead y el preview sobre el inicio real del clip en su nueva posición, igual que ya hacía el reorder por teclado.
- Se añadió una capa de subtítulos generados sobre el preview, activable desde un botón nuevo en la barra del panel de preview.
- El selector de subtítulos ahora abre un menú de estilos; por ahora quedó implementado el primer estilo, con texto base blanco y palabra activa en verde con agrandado y animación de pop.
- La estructura del menú ya deja visibles estilos futuros como `Typewriter`, `Fade In` y `Cut`, pero marcados como próximos para no mezclar placeholders con comportamientos todavía no resueltos.
- El overlay del preview dejó de mostrar el bloque completo de una sola vez y ahora avanza por cues temporales cortos, agrupando pocas palabras por ventana de tiempo mientras la animación sigue marcando únicamente la palabra en curso.
- Se corrigió un desacople en preview después de reordenar clips: los subtítulos del overlay estaban tomando el clip seleccionado en UI y no siempre el bloque real resuelto por el playhead, así que podían quedar visualmente corridos respecto al clip que estaba reproduciéndose.

### Decisiones tomadas

- Las sugerencias AI ya no deben materializarse como clips relacionales temporales en la secuencia activa del backend; la fuente de verdad pasa a ser el draft del editor porque allí viven reorder, metadata de publicación y estados no destructivos por secuencia.
- Cuando OpenAI no responde o devuelve un JSON insuficiente, el sistema debe seguir entregando 3 alternativas utilizables mediante fallback heurístico local en vez de dejar el flujo vacío.
- Al regenerar propuestas editoriales, conviene reemplazar solo las secuencias con `editorialSource = openai-editorial` y preservar intactas las secuencias manuales existentes.
- Para material tipo live, el número de piezas no debe ser fijo: conviene pensarlo como una densidad editorial aproximada de un highlight por cada ~90 segundos de material útil analizado.
- El sistema no debe proponer dos clips que compitan por el mismo tramo temporal; la depuración editorial tiene que repartir el live en picks distintos, no en variantes que se pisan entre sí.
- Si el objetivo es que el usuario entienda y supervise la resolución editorial, el agente no puede ser una caja negra: debe exponer sus iteraciones de descubrimiento, reescritura y consolidación en una capa UI dedicada.
- Para que la supervisión sea realmente útil, no basta con mostrar solo progreso; también hay que mostrar cómo muta cada candidato entre iteraciones, especialmente qué texto quedó fuera y cuándo una secuencia cambia de enfoque o de nombre.
- Cuando hay reorder editorial, la UI debe enseñar no solo que hubo cambio sino dónde ocurrió: una comparación compacta entre orden fuente y orden final hace mucho más auditable el trabajo del agente.

- El nombre de la secuencia seguirá funcionando como hook editorial, mientras que el `mainTitle` se guarda aparte porque cumple otra función: empaquetado visual y publicación.
- La descripción y los hashtags deben vivir dentro de cada secuencia y no a nivel global del proyecto, porque las variantes de 20s y 40s pueden requerir copys de publicación distintos.
- Las preferencias visuales del preview que alteran directamente la experiencia editorial deben persistirse junto con el layout; si viven solo en `useState`, cualquier refresh las borra aunque el resto del draft sí sobreviva.
- Cuando el problema restante es claramente de cómo se ve la forma de onda en pantalla, la corrección más estable es desplazar el dibujo renderizado y no volver a tocar el muestreo temporal de amplitudes.
- La waveform del timeline no debe depender de un MP4 de preview recodificado cuando el editor necesita precisión temporal; para sincronía fina, la referencia correcta es el audio fuente extraído sin una segunda compresión intermedia.
- El desfase que se percibía en audio no venía solamente del dato temporal sino también del dibujo: si cada barra deja margen vacío al inicio de su slot, el ataque del waveform parece entrar tarde aunque el tiempo real esté correcto.
- Los cambios de palabra activa deben usar intervalos semiabiertos `[start, end)` para evitar que, justo en el borde entre dos palabras, la UI siga mostrando la palabra anterior un frame más de lo debido.
- El reorder por drag y el reorder por teclado no podían seguir rutas distintas respecto al playhead: si el clip movido no se convierte inmediatamente en la referencia activa, el usuario percibe texto, audio y preview como si hubieran quedado corridos aunque el orden persistido sea correcto.
- El sistema de subtítulos debía arrancar como una capa del preview y no como parte fija del video, para poder iterar estilos visuales sin tocar todavía la exportación final.
- El primer estilo se montó sobre la palabra activa ya sincronizada por el playhead del transcript, así se evita duplicar otra heurística temporal y el highlight verde queda alineado con la reproducción real.
- Para legibilidad en formato vertical, era necesario cortar el texto en grupos temporales de pocas palabras en vez de intentar mostrar todo el clip; la unidad visual correcta del subtítulo es el cue activo, no el bloque entero.
- El bug observado tras reorder no venía del draft persistido sino de una fuente de estado equivocada en render: el overlay estaba leyendo `selectedTimelineBlock` antes que el bloque efectivamente resuelto desde `currentSequenceTimeMs`.

### Bloqueos o riesgos

- El uso de thumbnails embebidos en base64 dentro del prompt aumenta el peso de la llamada a OpenAI; si aparecen timeouts o límites de contexto en proyectos largos, habrá que reducir cantidad de frames o resumir mejor el transcript enviado.
- El fallback heurístico ya produce alternativas operables, pero la calidad editorial real seguirá dependiendo mucho más de la respuesta de OpenAI que de la heurística local.
- La aplicación actual consume `display_plan` por índice de bloque; si en el futuro la IA necesita controlar ajustes más finos por bloque, habrá que ampliar el contrato con crop, offset o referencias más explícitas a rangos.
- Aunque la cantidad ahora escala por duración, una llamada única con transcript muy largo y contexto visual amplio puede seguir siendo costosa para OpenAI; si un live completo empieza a tensar tiempo o contexto, tocará pasar a generación por lotes o por ventanas encadenadas.
- La primera versión del overlay muestra el avance por fases y candidatos, pero todavía no hace streaming fino palabra por palabra dentro de una misma secuencia provisional; si luego hace falta más granularidad, habrá que exponer diffs más detallados por iteración.
- El diff fino actual es por snapshot de iteración, no por cada microcambio interno del modelo; si más adelante se quiere ver una línea de tiempo todavía más granular, hará falta partir aún más las fases o introducir streaming estructurado del backend.

### Trabajo realizado

- Se compactó la revisión de sugeridas para acercarla al lenguaje del resto del editor: el modal ahora arranca con una toolbar superior del fragmento activo y deja visibles arriba las acciones `Ampliar rango` y `Aprobar corte`, en lugar de esconder la expansión de contexto en la parte baja del flujo.
- La columna de revisión se hizo menos vertical y más utilitaria: se redujeron paddings, gaps y alturas del preview, y `Orden narrativo` pasó a un bloque plegable para que la primera pantalla priorice texto, preview y decisión.
- El preview editorial sumó una barra clickeable de bloques/cortes debajo del visor para navegar explícitamente cada tramo del montaje, replicando mejor la lógica mental del editor de secuencias cuando se quiere inspeccionar o cambiar una escena.
- Se estabilizó el CSS del rediseño después de una inserción fallida de estilos compactos en una zona no relacionada del stylesheet; se restauraron las reglas afectadas y se reubicaron los estilos del review en su sección correcta.
- Se validó el estado final con `npm run build` sin errores ni advertencias CSS.

### Decisiones tomadas

- En la revisión editorial, las acciones críticas no pueden vivir bajo el fold: `Ampliar rango` debe verse arriba porque es parte del gesto principal de redepuración cuando el hallazgo quedó corto de contexto.
- El review de una sugerida debe comportarse más como una mesa de edición que como un panel técnico descriptivo; por eso conviene plegar explicaciones secundarias y dejar a la vista cortes, preview y aprobación.
- Si el usuario percibe que el preview no obedece bien los cortes, la UI tiene que exponer navegación directa por bloques, no solo confiar en autoplay o en que el usuario deduzca la estructura desde el texto.

### Bloqueos o riesgos

- Aunque la navegación por bloques ya acerca el preview al editor principal, todavía puede hacer falta reutilizar más del renderer de secuencias si el comportamiento visual sigue sintiéndose distinto en casos complejos de `fit/fill/split`.

### Próximo paso

- Probar manualmente una sugerida larga y confirmar que la toolbar superior, la compacidad del modal y la barra de bloques hacen innecesario el scroll inicial para expandir contexto o revisar cortes.

---

### Trabajo realizado

- El modal `Ampliar rango con más contexto` dejó de usar tarjetas grandes por párrafo y pasó a un visor vertical tipo documento, con scroll continuo y lectura corrida del texto disponible.
- La selección del rango ahora se hace arrastrando el mouse sobre los párrafos, como un resaltador: el sistema mantiene el rango actual como base y va absorbiendo contexto contiguo a medida que el usuario barre hacia arriba o hacia abajo.
- Cuando el barrido toca texto ya usado por otra sugerida, el modal lo comunica de forma explícita en el resumen inferior; si `fundir` está activo, ese texto pasa a quedar absorbible y la futura secuencia queda marcada como contexto compartido.
- Se redujo el peso visual del modal para que el texto gane protagonismo frente a controles y chips secundarios.
- Se validó el cambio con `npm run build` sin errores de frontend.

### Decisiones tomadas

- Para expandir contexto, la metáfora correcta no era una grilla de cards sino una página de texto sobre la que el usuario pueda barrer con el mouse de manera natural.
- En este flujo, los párrafos deben sentirse como texto seleccionable y no como botones; la UI secundaria solo acompaña con metadatos y alertas de solape.

### Bloqueos o riesgos

- La interacción actual replica un resaltado por arrastre entre párrafos, no una selección literal palabra a palabra; si más adelante hace falta granularidad más fina, habrá que bajar del nivel párrafo al nivel `word span` sin romper la regla de contigüidad.

### Próximo paso

- Probar manualmente si esta selección por barrido ya se siente suficientemente natural o si conviene avanzar a un resaltado todavía más fino dentro de cada párrafo.
- La mini-timeline actual resume bloques ya agrupados; si más adelante hace falta auditar microcambios dentro de un bloque largo, habrá que permitir zoom o abrir un detalle expandible por bloque.

- La compensación de 25 ms está calibrada contra el material actual; si más adelante aparecen fuentes con otra cadencia de ataque o distinta latencia base, quizá convenga exponer este ajuste como preferencia interna o recalibrarlo.
- Los proyectos que ya tengan `waveform.json` cacheado desde `seekable_preview` se regenerarán al volver a pedir assets porque el backend ahora solo reutiliza caché marcada como `source_audio`; eso corrige el offset, pero el primer refresco puede tardar un poco más.
- Si la transcripción base trae `start_ms` acústicamente conservadores o adelantados respecto al ataque real de voz, todavía podría quedar un pequeño desacople de origen en algunos términos; esta corrección elimina el sesgo de render, pero no rehace timings del transcript.
- Si el usuario espera mover un “módulo” compuesto por varios bloques derivados, todavía queda por resolver esa semántica de agrupación; esta corrección alinea playhead y preview con el bloque movido, pero no cambia el nivel de granularidad del reorder.
- Por ahora la activación y el estilo de subtítulos viven en estado local del preview; si luego se quiere persistir esa preferencia por proyecto o por secuencia, habrá que bajarla al draft.
- Los estilos futuros aparecen en el menú como roadmap visual, pero todavía no tienen implementación real; si eso confunde a usuarios finales, convendrá ocultarlos detrás de una etiqueta más explícita o un flag interno.
- Los cues usan una heurística fija de duración máxima, cantidad de palabras y caracteres; probablemente habrá que ajustar esos umbrales por estilo o por aspect ratio según se prueben más piezas reales.

### Próximo paso

- Probar manualmente una generación editorial completa sobre un proyecto real y revisar si las 3 secuencias creadas quedan suficientemente distintas en hook, duración y empaque de publicación.
- Si la calidad del prompt multimodal es consistente, exponer en UI una vista rápida de `mainTitle`, hashtags y descripción para editar el paquete de publicación sin salir de la secuencia.
- Probar manualmente dos escenarios: un rango de ~90 segundos para confirmar que sale aproximadamente una pieza fuerte, y un rango más largo para verificar que la cantidad crece sin generar secuencias montadas unas sobre otras.
- Probar manualmente el nuevo overlay del agente sobre `jinmy club 2026` para verificar que las iteraciones se sienten comprensibles, que el modal no tapa información crítica innecesariamente y que el cierre final aplica realmente las secuencias visibles en la última fase.
- Probar manualmente que las previews de `Seleccionado` y `Descartado` se correspondan con la versión final de cada secuencia para que el usuario pueda confiar en la lectura del diff y usarlo como auditoría editorial real.
- Probar manualmente que la mini-timeline marque en rojo solo los bloques realmente reordenados y que el paralelo `Origen` / `Final` sea suficiente para entender los casos donde el hook se mueve al principio o se compactan ideas en bloques nuevos.

- Verificar en el mismo punto donde quedaban unos ~25 ms de atraso que la cresta principal del waveform ya entra prácticamente alineada con la palabra y el playhead.
- Reabrir el timeline del proyecto para forzar la regeneración de la waveform cacheada y confirmar visualmente que el offset fijo cercano a 50 ms desapareció en ataques cortos de voz.
- Validar manualmente varios comienzos de palabra con ataque fuerte de audio para confirmar que la cresta del waveform ya no parece entrar tarde respecto al playhead y que el cambio de palabra activa no se queda pegado al token previo en bordes exactos.
- Validar manualmente un reorder por drag sobre clips largos para confirmar que el preview, los subtítulos y el audio ahora arrancan desde el inicio del bloque movido y no desde la posición absoluta anterior del playhead.
- Validar manualmente el primer estilo sobre clips largos y cortos para ajustar tamaño, saltos de línea y la intensidad exacta del agrandado de la palabra activa.
- Validar manualmente si la duración y densidad de cada cue se sienten naturales en vertical y horizontal, y ajustar los umbrales si alguna frase todavía entra demasiado cargada.

---

### Trabajo realizado

- Se añadió edición inline de palabras del transcript por doble click: aparece un popover pequeño junto a la palabra, permite escribir una o varias palabras y confirma con `Enter`.
- El texto original de cada palabra se conserva siempre por separado; si se vuelve a abrir la corrección, el texto entra totalmente seleccionado y `Backspace` con esa selección restaura el contenido original antes de confirmar con `Enter`.
- Las correcciones visuales del transcript ahora se reflejan también en la secuencia derivada y se guardan dentro del draft del proyecto para sobrevivir recargas y reaperturas.

### Decisiones tomadas

- La corrección debía vivir como override no destructivo sobre el transcript original, para permitir retoques editoriales rápidos sin perder la referencia transcrita de base.
- El gesto de doble click y el popover contextual reducen fricción respecto a abrir un modal grande, porque la corrección ocurre exactamente en el lugar donde el usuario detecta la errata.

### Bloqueos o riesgos

- Si una retranscripción futura cambia completamente los `wordId`, las correcciones viejas dejarán de aplicar; por ahora se filtran automáticamente y solo sobreviven cuando la palabra base sigue existiendo.
- Las correcciones de texto no entran todavía en el historial de undo/redo; se guardan como estado persistente de draft, pero no como snapshot histórico.

### Próximo paso

- Validar manualmente la experiencia de doble click tanto en el transcript fuente como en el de secuencia, especialmente el caso de restaurar al original con `Backspace` y confirmar con `Enter`.

---

### Trabajo realizado

- Se añadieron atajos de teclado para navegar y editar la timeline sin depender del mouse: `Home` lleva al inicio, `End` al final, `Page Up` y `Page Down` saltan entre inicios de clips, `,` y `.` controlan el zoom, las flechas seleccionan clips y `Shift` más flechas reordenan el clip seleccionado.
- El marcado naranja persistente del reorder dejó de basarse en comparar toda la secuencia contra el orden original y pasó a persistir únicamente los bloques movidos explícitamente por el usuario.
- Se reforzó levemente la lectura visual del handle superior de reorder con una pequeña guía bajo la pestaña, para conectarlo mejor con el bloque del clip.
- El reorder por teclado ahora muestra un aviso efímero animado en la cabecera del preview, con entrada y salida suaves en lugar de aparecer de golpe.

### Decisiones tomadas

- Para un flujo editorial más rápido, la timeline debe poder recorrerse, enfocarse y reordenarse también desde teclado; eso reduce fricción cuando el usuario ya tiene un clip seleccionado y no quiere volver a apuntar con el cursor.
- El color naranja persistente debe responder a la intención del usuario y no a todos los clips desplazados como efecto colateral del reorder; si no, el feedback termina exagerando el cambio real.

### Bloqueos o riesgos

- Los nuevos atajos comparten el listener global del editor; si más adelante se agregan inputs o paneles con foco complejo, habrá que vigilar conflictos para no capturar teclas cuando el usuario esté escribiendo.
- Los `movedBlockIds` ahora forman parte del estado serializado de la secuencia; si cambia el modelo de persistencia del draft, habrá que mantener esta propiedad en la migración para no perder el marcado visual.

### Próximo paso

- Validar manualmente que los atajos no interfieran con otros focos del editor y confirmar que, después de un reorder, solo permanezca naranja el clip movido explícitamente.

---

### Trabajo realizado

- Se añadió una ayuda de descubrimiento sobre la pestaña de reorder del clip: tooltip corto en hover y una pulsación sutil que aparece una sola vez cuando el usuario selecciona un clip por primera vez.

### Decisiones tomadas

- La discoverability del gesto mejora más cuando la pista es contextual y temporal; mostrarla una sola vez evita ruido permanente pero sigue enseñando desde dónde se mueve el clip.

### Bloqueos o riesgos

- El hint se guarda en `localStorage` como visto; si luego se cambia mucho el diseño del handle, habrá que versionar esa clave para volver a mostrar la ayuda una vez.

### Próximo paso

- Validar manualmente que la pista aparezca en el momento adecuado y no tape información importante en clips muy angostos.

---

### Trabajo realizado

- Se reforzó la ayuda visual de la pestaña superior del clip para que se entienda mejor como handle de reorder: ahora es más visible, tiene grip de puntos y etiqueta textual de mover.

### Decisiones tomadas

- Si el usuario no identifica rápidamente desde dónde se arrastra, el gesto de long-press pierde discoverability; la pestaña debía parecer una pieza “tomable” y no solo un adorno del clip.

### Bloqueos o riesgos

- La pestaña ahora ocupa más ancho sobre el clip; si en bloques muy angostos se siente invasiva, habrá que reducirla o mostrar una versión compacta según el tamaño del bloque.

### Próximo paso

- Validar manualmente si la discoverability del handle ya es suficiente o si todavía conviene sumar una pista contextual adicional en hover o primera interacción.

---

### Trabajo realizado

- Se añadió una pestaña superior centrada en cada clip para entrar al reorder con long-press: al mantenerla presionada, el bloque pasa a modo flotante, se eleva visualmente y empieza a empujar/recolocar a los clips vecinos.
- Durante ese drag flotante el timeline ahora puede autodesplazarse hacia izquierda o derecha cuando el cursor se acerca a los bordes, facilitando ubicar el clip más lejos sin soltar.
- El bloque levantado usa un naranja claro de estado “en el aire” y, al aterrizar tras un reorder real, pasa brevemente a un color distinto antes de quedar con su marcado persistente.

### Decisiones tomadas

- El reorder directo desde cualquier punto del clip resultaba ambiguo; separar el gesto en una pestaña dedicada con hold mejora la intención del usuario y acerca la interacción a un drag and drop editorial más controlado.

### Bloqueos o riesgos

- El long-press actual usa un umbral fijo; si luego se siente lento o demasiado sensible, convendrá ajustar ese tiempo o cancelarlo también por movimiento mínimo del cursor.

### Próximo paso

- Validar manualmente si la pestaña superior se siente natural y si el autoscroll durante el drag flotante tiene suficiente velocidad cerca de los bordes.

---

### Trabajo realizado

- Se reforzó el drag and drop de reorder en timeline para que funcione visualmente como un sortable horizontal: mientras el usuario arrastra un clip, los bloques vecinos se recolocan en vivo y muestran el intercalado antes de soltar.
- El feedback naranja anterior se mantuvo, pero ahora se apoya además en un desplazamiento real de las piezas para que el cambio de orden sea legible incluso antes del drop.

### Decisiones tomadas

- En un reorder de tipo lista, solo cambiar color no basta; el usuario necesita ver el hueco y el corrimiento de los elementos contiguos para entender de inmediato dónde va a caer el bloque arrastrado.

### Bloqueos o riesgos

- Esta capa todavía usa una previsualización discreta por posiciones, no un arrastre físico libre del bloque con interpolación completa; si luego se busca una sensación más sofisticada, se puede sumar una línea fantasma o un follow más elástico del clip arrastrado.

### Próximo paso

- Validar manualmente si el intercalado en vivo ya transmite con claridad el destino del clip o si conviene añadir un indicador de inserción todavía más explícito.

---

### Trabajo realizado

- El estado visual de clips reordenados también pasó al módulo de transcript: los bloques fuera de su orden original ahora se marcan allí con acento naranja.
- Se añadió un botón pequeño de reversión sobre el badge del bloque reordenado para devolver ese bloque a su posición original directamente desde el transcript.

### Decisiones tomadas

- El reorder no debe vivir solo en la timeline; si también afecta cómo se lee la secuencia textual, el transcript tiene que mostrar esa misma señal y ofrecer una salida rápida para deshacer ese cambio puntual sin depender del drag en la timeline.

### Bloqueos o riesgos

- La reversión devuelve el bloque a su posición natural de origen dentro del orden actual; si más adelante se introducen reglas más complejas de orden base, habrá que ajustar esa noción de “posición original”.

### Próximo paso

- Validar manualmente que el badge naranja y el botón de revertir se mantengan claros incluso en párrafos densos del transcript.

---

### Trabajo realizado

- Se ajustó el botón de reproducción de secuencia para que, si el playhead ya quedó al final del timeline, al volver a presionar Play reinicie desde el inicio en vez de intentar reproducir desde el último frame.

### Decisiones tomadas

- El comportamiento esperado después de terminar una reproducción completa es “replay”; mantener el playhead al final pero reusar ese mismo punto como inicio genera una falsa reproducción vacía.

### Bloqueos o riesgos

- El reinicio automático aplica cuando el playhead cae dentro de la tolerancia del final de la secuencia; si más adelante se quiere distinguir entre final exacto y pausa manual cerca del final, habrá que separar esos dos casos.

### Próximo paso

- Validar manualmente que al terminar la secuencia completa el siguiente Play arranque desde 0 tanto con el botón como con el atajo de teclado.

---

### Trabajo realizado

- Se añadió autoscroll del timeline durante la reproducción: cuando el playhead entra en la zona derecha del viewport, la vista avanza automáticamente para volver a dejar campo visible hacia delante.
- El seguimiento no centra la línea exacta todo el tiempo; la devuelve a una franja más a la izquierda para que el usuario siga viendo hacia dónde avanza la secuencia y qué bloques vienen después.

### Decisiones tomadas

- Para lectura editorial, es mejor mantener el playhead en una zona de anticipación que perseguirlo exactamente en el centro; eso deja más contexto útil a la derecha y evita la sensación de ir “a ciegas” cuando la línea toca el borde.

### Bloqueos o riesgos

- Si luego se quiere una sensación más cinematográfica, se puede suavizar todavía más el avance con interpolación; por ahora se priorizó un comportamiento claro y estable durante playback.

### Próximo paso

- Validar manualmente reproducciones largas para ajustar, si hace falta, el porcentaje exacto donde se dispara el autoscroll y la cantidad de campo visible que se deja por delante.

---

### Trabajo realizado

- Se ajustó el karaoke de la pista de video para que la palabra activa se ubique en el centro visible real del clip cuando el bloque queda parcialmente fuera del viewport horizontal.
- El cálculo del foco del strip y de la caja central ahora usa `timelineScrollLeft` + `timelineViewportWidth` en lugar del centro absoluto del bloque.

### Decisiones tomadas

- Si el usuario está viendo solo una porción del clip, el ancla visual debe responder a esa porción visible y no al ancho total del bloque; de lo contrario, la palabra “centrada” se siente corrida cuando el clip entra o sale del viewport.

### Bloqueos o riesgos

- En clips extremadamente angostos la caja central sigue limitada por el ancho disponible del propio bloque, así que el texto puede comprimirse aunque el ancla sea correcta.

### Próximo paso

- Validar manualmente el comportamiento al hacer scroll horizontal con un clip largo para comprobar que la palabra activa se mantenga en el centro de la parte visible del bloque.

---

### Trabajo realizado

- Se corrigió la fuente de la waveform del timeline para que deje de construirse desde el audio extraído del video original y pase a generarse desde el mismo preview seekable que reproduce el editor.
- La caché vieja de waveform queda invalidada automáticamente si todavía viene de la fuente anterior, así que el timeline regenera una versión alineada la próxima vez que pida assets.

### Decisiones tomadas

- Si el usuario compara waveform contra el video que realmente ve en preview, ambos deben nacer del mismo medio; mezclar preview seekable y waveform del archivo original deja abierta la puerta a offsets fijos difíciles de compensar de forma fiable en frontend.

### Bloqueos o riesgos

- La primera carga después del cambio puede tardar un poco más porque se regenerará la waveform cacheada desde el preview seekable.

### Próximo paso

- Validar manualmente en la timeline que los picos de audio vuelvan a caer sobre el mismo momento que muestra el preview, especialmente en consonantes fuertes o golpes visibles.

---

### Trabajo realizado

- Se añadió una primera capa visual al reorder manual de clips en la timeline: el clip arrastrado y el punto de intercalado ahora se marcan en naranja durante el drag.
- Los bloques que ya no quedaron en su posición original pasan a conservar un estado visual distinto para que el reordenamiento siga siendo legible después de soltar.

### Decisiones tomadas

- Esta capa debía montarse sobre la lógica de `move-block` ya existente, sin tocar todavía el algoritmo de reorder, para separar claramente comportamiento de negocio y feedback visual.

### Bloqueos o riesgos

- El criterio de “posición original” se está infiriendo por el orden natural de entrada del material; si después se define otra noción de orden base, habrá que ajustar ese cálculo para que el marcado persistente siga siendo correcto.

### Próximo paso

- Validar manualmente si el highlight naranja transmite con suficiente claridad cómo se intercalará el clip y decidir la siguiente capa visual del reorder.

---

### Trabajo realizado

- Se reemplazó la lógica estimada del karaoke de timeline por la misma referencia exacta que usa el cursor del transcript: la palabra activa ahora se resuelve con el mismo `playheadTranscriptItemId`.

### Decisiones tomadas

- Si el cursor del transcript ya está bien sincronizado, la timeline debe colgarse de esa misma señal en vez de reconstruir otra heurística temporal propia, porque cualquier interpolación adicional puede adelantarse o retrasarse respecto a la voz real.

### Bloqueos o riesgos

- El strip de la timeline ya no anticipa la palabra siguiente; si luego se quiere recuperar algo de fluidez visual, habrá que hacerlo sin romper la correspondencia exacta con el cursor del transcript.

### Próximo paso

- Verificar manualmente que al pausar la reproducción la palabra destacada en timeline coincida exactamente con la marcada en el transcript.

---

## 2026-03-23

### Trabajo realizado

- Se corrigió la lógica temporal del karaoke en la pista de video para que el desplazamiento no avance a velocidad uniforme por palabra, sino interpolando con los tiempos reales de pronunciación entre una palabra y la siguiente.

### Decisiones tomadas

- El scroll del karaoke debe derivarse de `start_ms` y `end_ms` de cada palabra; usar solo la palabra activa produce saltos visuales y una sensación de velocidad artificial que no acompaña bien la voz.

### Bloqueos o riesgos

- La interpolación usa los tiempos disponibles por palabra; si alguna palabra llega sin timings fiables, el movimiento en ese tramo seguirá dependiendo de los fallbacks temporales del bloque.

### Próximo paso

- Comprobar clips con palabras muy largas y muy cortas para verificar que el cambio de velocidad se sienta natural respecto al audio.

---

## 2026-03-23

### Trabajo realizado

- El efecto karaoke dejó de vivir en la fila superior `T` y pasó al texto blanco del clip en la pista de video.
- La palabra activa ahora se desplaza para quedar alineada visualmente con la posición del playhead dentro del bloque y se destaca con más peso tipográfico.

### Decisiones tomadas

- La pista de video es el lugar correcto para este comportamiento porque es la referencia visual directa del clip; la fila `T` vuelve a quedar como resumen simple del bloque.

### Bloqueos o riesgos

- La alineación de la palabra activa con el playhead sigue basada en estimación de ancho por palabra; si se busca precisión tipográfica absoluta, habrá que medir el texto renderizado.

### Próximo paso

- Validar que el ritmo visual del texto blanco acompañe bien la voz y que la palabra activa quede suficientemente cerca del playhead en distintos niveles de zoom.

---

## 2026-03-23

### Trabajo realizado

- Se transformó la fila `T` de la timeline en una tira tipo karaoke: las palabras del clip ahora se desplazan de derecha a izquierda dentro del ancho del bloque y la palabra activa queda resaltada según el tiempo actual del clip.

### Decisiones tomadas

- La fila de texto dejó de mostrar solo el `label` del bloque y pasó a usar las palabras reales del clip, porque eso permite que la pausa y el playhead se lean contra el mismo contenido textual que se está diciendo en audio y video.

### Bloqueos o riesgos

- El ancho del scroll se estima a partir del largo de las palabras, no de medición real de tipografía; si luego se quiere una coincidencia pixel-perfect, habrá que medir el ancho renderizado del texto.

### Próximo paso

- Validar visualmente que el highlight de la palabra activa coincida con la voz en clips largos y rápidos.

---

## 2026-03-23

### Trabajo realizado

- Se unificó el render del video principal del preview para que el nodo `<video>` principal permanezca montado al cambiar entre `Split`, `Fit` y `Fill`.
- La resincronización especial quedó relegada solo a los videos auxiliares (`backdrop` de `Fit` y secundario de `Split`), eliminando el remount del video principal que provocaba congelarse en el primer frame al pasar de `Split` a `Fit`.

### Decisiones tomadas

- La transición verdaderamente seamless entre modos exige preservar el nodo del video principal; esconderlo y volver a sincronizarlo después de montarlo seguía dejando una ventana donde podía verse o quedar clavado en el primer frame.

### Bloqueos o riesgos

- El preview ahora comparte más estructura entre modos, así que cualquier futuro cambio de layout en `Split` tendrá que respetar ese nodo primario persistente para no reintroducir el problema.

### Próximo paso

- Validar manualmente `Split -> Fit`, `Split -> Fill` y `Fit -> Split` tanto en pausa como en reproducción continua.

---

## 2026-03-23

### Trabajo realizado

- Se corrigió una regresión que pausaba la reproducción al entrar al siguiente clip: el playback se estaba cancelando por cualquier cambio de clip seleccionado, incluso durante transiciones automáticas entre bloques.
- La resincronización visual del preview quedó limitada a cambios de modo que realmente entran o salen de `Split`; `Fit <-> Fill` vuelve a comportarse sin pausa ni compuerta extra.

### Decisiones tomadas

- La reproducción automática no debe depender del estado de selección del clip, porque el propio motor de playback actualiza esa selección al avanzar entre bloques.

### Bloqueos o riesgos

- La transición `Split <-> otro modo` sigue siendo la única ruta especial; si reaparece un frame incorrecto ahí, habrá que mantener el nodo primario persistente entre ramas en vez de resincronizarlo tras montar.

### Próximo paso

- Validar que `Fill -> Fit`, `Fit -> Fill` y avance automático entre clips ya no se queden pausados en el primer frame.

---

## 2026-03-23

### Trabajo realizado

- Se corrigió un solapamiento visual en `Split`: al seleccionar el panel inferior, el superior podía parecer recortado porque el fondo del panel activo se pintaba encima del overflow del otro clip.

### Decisiones tomadas

- En `Split`, los paneles no deben aportar una capa de fondo opaca propia; solo deben servir como superficie de interacción y contenedor, para no ocultar el contenido libre del clip vecino.

### Bloqueos o riesgos

- Si luego se quiere volver a dar identidad visual a cada mitad del `Split`, habrá que hacerlo con una capa que no interfiera con el overflow de los frames.

### Próximo paso

- Verificar manualmente que al hacer clic en el video inferior el superior ya no se “cropee” visualmente.

---

## 2026-03-23

### Trabajo realizado

- Se refinó la transición entre clips para que la resincronización visual solo ocurra cuando cambia el modo del mismo clip, no cuando la reproducción avanza automáticamente al siguiente bloque.
- Se reforzó el apilado interno de `Split` para que el frame activo quede por encima del inactivo y un panel no tape visualmente al otro al seleccionarlo.

### Decisiones tomadas

- La transición `Split -> siguiente clip` no debe reutilizar la compuerta visual de cambio manual de modo, porque eso introduce negro y falsa pausa al inicio del bloque siguiente.

### Bloqueos o riesgos

- Si reaparece un solapamiento raro en `Split`, habrá que revisar si conviene desacoplar todavía más el stacking del panel y el stacking del frame interno.

### Próximo paso

- Probar secuencias `Split -> Fill`, `Split -> Fit` y selección manual del panel inferior dentro de `Split` para confirmar que ya no hay negro ni solapado incorrecto.

---

## 2026-03-23

### Trabajo realizado

- Se corrigió una fuga de reproducción al cambiar de clip seleccionado: ahora cualquier playback activo del preview se cancela por completo y se pausan también los videos auxiliares del preview para evitar audio doble o desfasado.
- Se limitó la transición visual de resincronización a cambios reales de modo, evitando pantallas negras innecesarias en simples cambios de clip.

### Decisiones tomadas

- El cambio de clip debe invalidar la sesión de playback anterior aunque React todavía tenga promesas de `seek/play` pendientes, porque de otro modo un video viejo puede volver a arrancar por detrás.

### Bloqueos o riesgos

- Si más adelante se añaden nuevos nodos de video al preview, deberán pasar también por la misma rutina centralizada de pausa para no reintroducir audio fantasma.

### Próximo paso

- Validar manualmente cambios rápidos entre clips con combinaciones mixtas `Fill`, `Fit` y `Split`, especialmente mientras una secuencia ya está reproduciéndose.

---

## 2026-03-23

### Trabajo realizado

- Se corrigió el flash del primer frame al cambiar un clip desde `Split` hacia `Fill` o `Fit` ocultando temporalmente los nodos de preview recién montados hasta resincronizarlos con `previewSourceMs`.

### Decisiones tomadas

- El cambio de modo visual debe tratarse como una transición de montaje del preview, no solo como un cambio de estilos, porque React puede pintar el frame 0 antes de que corra el seek.

### Bloqueos o riesgos

- La solución introduce una ocultación muy breve durante la resincronización; si en otro navegador apareciera parpadeo adicional, habrá que mover esta lógica a una capa de preview persistente en lugar de recrear nodos por modo.

### Próximo paso

- Validar manualmente cambios rápidos `Split -> Fill`, `Split -> Fit` y `Fit -> Split` con el video en pausa y en reproducción.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió el apilado visual en `Split` para que el panel seleccionado quede por encima de su hermano cuando un elemento cruza la frontera entre ambas mitades.

### Decisiones tomadas

- En composición libre de `Split`, el elemento activo debe ganar prioridad visual de pintado; de lo contrario parece “recortado” aunque el crop ya no lo esté limitando.

### Bloqueos o riesgos

- Si en el futuro se editan ambos paneles al mismo tiempo, habrá que definir una política de apilado más explícita que el simple `panel activo arriba`.

### Próximo paso

- Validar manualmente que el clip superior ya pueda bajar sobre la zona inferior sin quedar tapado por el panel de abajo.

---

## 2026-03-22

### Trabajo realizado

- Se eliminó el clipping por panel en `Split` para que cada elemento pueda desplazarse libremente por el canvas sin empezar a recortarse al salir de su mitad original.
- El drag y la escala en `Split` ahora toman como referencia el `stage` completo del preview, no solo el panel local.

### Decisiones tomadas

- Una vez definido el crop de cada elemento en `Split`, su composición debe resolverse sobre el canvas completo y no quedar limitada por los bordes internos de cada panel.

### Bloqueos o riesgos

- Si más adelante se quiere un submodo de `Split` con límites estrictos por panel, eso debería configurarse aparte y no reutilizar esta interacción libre.

### Próximo paso

- Validar manualmente que mover el clip superior hacia abajo o el inferior hacia arriba ya no provoque recorte por bordes del panel y que ambos se comporten como elementos libres sobre el canvas.

---

## 2026-03-22

### Trabajo realizado

- Se alineó el modo `Split` con la misma semántica visual de `Fit`: cada panel ahora renderiza un frame editable basado en `cropRect`, con su propia escala del frame y su propio movimiento dentro del panel.
- Se eliminó la dependencia del render tipo `cover` fijo dentro de cada mitad, que estaba forzando encuadres parecidos a `1:1` y haciendo que la imagen se achicara dentro de un recuadro que no acompañaba la escala.

### Decisiones tomadas

- Conceptualmente, `Split` pasa a tratarse como dos instancias independientes de `Fit` sobre dos paneles, no como dos videos dibujados con un `cover` simplificado.

### Bloqueos o riesgos

- Si luego se desea una variante de `Split` con comportamiento distinto a `Fit`, convendrá introducir un submodo explícito en vez de reutilizar esta misma semántica.

### Próximo paso

- Validar manualmente que en `Split` el crop aplicado en cada panel se refleje exactamente, que el frame acompañe la escala y que cada panel pueda moverse libremente dentro de su mitad.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la edición en modo `Split` para que los paneles `primary` y `secondary` sean realmente independientes en selección y preview.
- Ahora ambos paneles aceptan click, muestran su propio overlay de selección y usan sus propios ajustes de crop, offset y escala al renderizar.
- El modal de crop y las operaciones de mover/escalar quedan dirigidas al panel actualmente seleccionado, en lugar de escribir siempre sobre el panel superior.

### Decisiones tomadas

- En `Split`, cada panel debe comportarse como un elemento editable independiente aunque ambos apunten al mismo video fuente.

### Bloqueos o riesgos

- Si más adelante se quiere edición simultánea o sincronizada entre paneles `Split`, habrá que añadir una capa explícita de linking; por defecto permanecen desacoplados.

### Próximo paso

- Validar manualmente que se pueda seleccionar tanto el panel superior como el inferior, abrir crop en cada uno y ver reflejado el resultado en ese panel específico.

---

## 2026-03-22

### Trabajo realizado

- Se ajustó el preset por defecto del crop para que, en `Fill`, siga el aspecto activo del canvas en lugar de arrancar en un encuadre genérico.
- En la práctica: `9:16 + Fill` abre el crop con preset `9:16`, `1:1 + Fill` con preset `1:1`, y `16:9 + Fill` con preset `16:9`.

### Decisiones tomadas

- El crop inicial debe reforzar visualmente el formato de salida elegido para evitar confusión al definir el área de interés, sobre todo en canvas verticales.

### Bloqueos o riesgos

- Los clips que ya tengan un crop guardado seguirán respetando ese valor; este cambio afecta el valor por defecto para estados nuevos o cuando se usa `Reset`.

### Próximo paso

- Validar manualmente que en `9:16 + Fill` el modal de crop ya se abra mostrando un recorte `9:16` evidente y coherente con el canvas.

---

## 2026-03-22

### Trabajo realizado

- Se añadieron guías visuales de snap al centro del canvas en el preview para elementos `Fit`.
- Al mover el frame, el editor ahora detecta alineación con el centro horizontal, vertical o ambos, ajusta el eje correspondiente a `0` y muestra la línea de guía mientras dura el drag.

### Decisiones tomadas

- El snap de centrado debe actuar solo como ayuda temporal durante el movimiento y desaparecer al soltar el puntero.

### Bloqueos o riesgos

- El umbral de snap puede requerir un ajuste fino posterior según la sensación real de uso.

### Próximo paso

- Validar manualmente que al acercar el elemento al centro aparezcan las guías vertical, horizontal o ambas, y que el frame quede alineado con precisión.

---

## 2026-03-22

### Trabajo realizado

- Se separó la persistencia visual de cada clip por aspecto de canvas (`16:9`, `9:16`, `1:1`) para que cada uno conserve su propio `fill/fit/split`, crop, offsets y escala.
- La lectura mantiene compatibilidad hacia atrás con proyectos que todavía guardan el formato viejo no segmentado por aspecto, usándolo como fallback hasta que el clip se vuelva a editar.

### Decisiones tomadas

- El modo visual y las coordenadas de un clip ya no deben compartirse entre aspectos distintos del canvas, porque el mismo encuadre no representa la misma composición en `16:9`, `9:16` y `1:1`.

### Bloqueos o riesgos

- Los clips que ya tengan ajustes viejos migrarán de forma perezosa: el valor antiguo se usa como base al entrar en un aspecto y recién después se reescribe en el nuevo formato segmentado.

### Próximo paso

- Validar manualmente que un clip pueda tener, por ejemplo, `Fill` en `16:9`, `Fit` en `9:16` y otro crop en `1:1` sin contaminarse entre sí al alternar el selector de aspecto.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió el render interno de `Fit` para que el video recortado conserve siempre su relación de aspecto original también en canvas `16:9`.
- El preview dejó de forzar ancho y alto independientes sobre el `<video>` recortado; ahora el tamaño se resuelve desde el aspecto nativo del video y el crop solo controla ventana visible y posición.

### Decisiones tomadas

- En `Fit`, el crop puede cambiar la ventana visible, pero nunca debe estirar el contenido fuente ni romper su proporción.

### Bloqueos o riesgos

- Si aparece alguna desalineación fina entre el modal y el preview después de este cambio, el ajuste pendiente ya no sería de deformación sino de mapeo de offsets.

### Próximo paso

- Validar manualmente en `16:9` que al aplicar `Fit + Crop` el sujeto y los objetos no se vean estirados ni aplastados en ningún tamaño de frame.

---

## 2026-03-22

### Trabajo realizado

- Se eliminó el tope que obligaba al frame `Fit` a quedarse dentro del `100%` del canvas en ancho/alto, habilitando un ajuste realmente libre por encima o por debajo del tamaño base del preview.
- El rango de `scale` manual también se amplió para permitir clips mucho más pequeños o mucho más grandes sin perder proporción.

### Decisiones tomadas

- El modo `Fit` debe permitir que el usuario sobredimensione o reduzca el frame a discreción; el canvas puede recortar visualmente el exceso, pero no debe bloquear el gesto de escala.

### Bloqueos o riesgos

- Con frames muy grandes, algunos handles pueden quedar fuera del área visible del canvas mientras el clip siga seleccionado; si eso molesta en uso real, tocará añadir una capa de controles desacoplada del borde visible.

### Próximo paso

- Validar manualmente que ahora el frame `Fit` pueda sobrepasar el ancho del canvas hacia izquierda/derecha manteniendo proporción, y también reducirse a tamaños más pequeños que antes.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió el escalado del frame visible en `Fit` para que use un único factor uniforme cuando llega al límite del contenedor, evitando que ancho y alto se clampen por separado.
- También se ajustó la matemática del drag de resize para que los handles sigan el eje dominante y mantengan una sensación de escalado proporcional más estable.

### Decisiones tomadas

- En `Fit`, al llegar al tamaño máximo disponible, el frame debe dejar de crecer sin deformarse; no debe cambiar de criterio y empezar a “estirarse” por el otro eje.

### Bloqueos o riesgos

- Si más adelante se quiere permitir sobre-escalar un frame `Fit` por fuera del canvas con recorte adicional, habrá que separar explícitamente ese comportamiento del actual `fit-inside`.

### Próximo paso

- Validar manualmente que al arrastrar los handles más allá del área disponible el frame simplemente se tope manteniendo proporción, sin volverse más alto o más ancho de forma inesperada.

---

## 2026-03-22

### Trabajo realizado

- Se restauró la rama de render específica de `Fit` en el preview, que se había perdido en una iteración previa y hacía que todos los modos no-`Split` terminaran renderizando como `Fill`.
- Con esto, el canvas vuelve a usar el frame, backdrop y mapeo de coordenadas propios de `Fit` cuando corresponde.

### Decisiones tomadas

- `Fit` no puede compartir sin más la misma rama visual de `Fill`, porque su semántica depende del `cropRect`, del frame visible y del blur de fondo.

### Bloqueos o riesgos

- Si aparecen diferencias nuevas entre el render de edición y el render de reproducción, habrá que seguir consolidando la rama compartida sin perder la semántica propia de cada modo.

### Próximo paso

- Validar manualmente que el modo `Fit` vuelva a responder a las coordenadas del modal y no se vea como `Fill`.

---

## 2026-03-22

### Trabajo realizado

- Se estabilizó el nodo principal de video del preview entre modos `Fit` y `Fill` para evitar desmontajes/re-montajes al cambiar de un clip al siguiente con modos visuales distintos.
- El backdrop blur de `Fit` pasó a activarse por visibilidad, pero el `<video>` principal permanece en la misma posición estructural del árbol React.

### Decisiones tomadas

- Los cambios de modo visual entre clips consecutivos no deben recrear el elemento de video principal durante playback, porque eso introduce riesgo alto de freeze visual mientras el audio sigue corriendo.

### Bloqueos o riesgos

- La ruta `Split` todavía conserva una rama de render separada; si aparecen freezes específicamente al entrar o salir de `Split`, habrá que aplicar la misma estabilización allí.

### Próximo paso

- Validar manualmente una secuencia que alterne `Fill` y `Fit` entre bloques para confirmar que el preview ya no se queda clavado en el frame de intersección.

---

## 2026-03-22

### Trabajo realizado

- Se añadió una espera explícita de frame de video presentado tras `seek + play` en las transiciones entre clips.
- Si el navegador reproduce audio pero no presenta un frame nuevo a tiempo, el flujo reintenta la transición con un pequeño nudge temporal para destrabar la imagen congelada.

### Decisiones tomadas

- Para secuencias multi-clip, no basta con validar `currentTime`; hay que validar también que el video haya presentado un frame nuevo cuando se salta de un bloque a otro.

### Bloqueos o riesgos

- Si el problema proviene de una limitación más profunda del archivo de preview o del decoder, este mecanismo mejora la recuperación pero puede no eliminar todos los casos extremos.

### Próximo paso

- Verificar manualmente que al entrar al siguiente clip ya no continúe el audio con la imagen clavada en el frame de intersección.

---

## 2026-03-22

### Trabajo realizado

- Se reforzó la transición de reproducción entre clips consecutivos para evitar que el preview quede congelado en el primer frame del siguiente bloque después del `seek`.
- Al detectar que el video no avanza tras el salto al siguiente clip, el reproductor ahora reintenta la reproducción con una pequeña reactivación temporal en lugar de quedar detenido hasta que el usuario haga scrub.

### Decisiones tomadas

- El cambio de bloque durante playback debe considerarse una ruta especial de reanudación, porque algunos navegadores pueden quedarse en el frame inicial justo después de un `seek` + `play` rápido.

### Bloqueos o riesgos

- Si el archivo de preview o el navegador tienen problemas más profundos de decodificación en seeks consecutivos, este fix reduce el síntoma pero podría requerir una estrategia de reproducción aún más específica.

### Próximo paso

- Validar manualmente reproducción continua en secuencia para confirmar que cada salto al siguiente clip ya no se congela en su primer frame.

---

## 2026-03-22

### Trabajo realizado

- En modo `Fit`, el preview dejó de depender solo de `object-position` y ahora mapea el `cropRect` del modal a coordenadas directas del video dentro del frame visible.
- Esto permite que mover el área de interés en el modal sí desplace la región visible aplicada en canvas, en lugar de recentrarla artificialmente.
- También se habilitó la reactivación de la herramienta de selección al hacer click en cualquier parte del foreground de `Fit`, no solo dentro del frame visible actual.

### Decisiones tomadas

- El modal de crop pasa a ser la fuente de verdad de la ventana visible en `Fit`; el canvas debe obedecer exactamente esas coordenadas.

### Bloqueos o riesgos

- Todavía puede hacer falta un refinamiento adicional de límites y sensación de arrastre para que la interacción se sienta idéntica a una herramienta de crop profesional.

### Próximo paso

- Validar manualmente que mover el crop a otra zona del video se vea reflejado inmediatamente al aplicar y que la herramienta no “desaparezca” al clickear fuera del frame visible.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la geometría del modo `Fit` para que el frame visible del canvas refleje la relación de aspecto real del `cropRect` aplicado desde el modal.
- En `Fit`, el escalado manual del overlay ahora afecta el tamaño del perímetro/frame visible en lugar de encoger solo el video interno.
- El video interno de `Fit` dejó de mezclar el `scale` manual con el `cropZoom`, evitando el efecto de imagen pequeña dentro de un marco que no responde.

### Decisiones tomadas

- En `Fit`, el marco visible debe representar la salida compuesta del clip y no permanecer fijo mientras el contenido interno cambia de tamaño.

### Bloqueos o riesgos

- Aún puede hacer falta un segundo ajuste fino para diferenciar totalmente el comportamiento por relación de aspecto del canvas (`16:9` vs `9:16`) en los presets y en los límites de interacción.

### Próximo paso

- Validar manualmente en `9:16` que aplicar `1:1` desde el modal convierta el frame visible en un cuadrado real y que los handles redimensionen ese mismo frame.

---

## 2026-03-22

### Trabajo realizado

- El slider de zoom de la timeline ahora usa el playhead actual como centro de expansión/contracción, igual que el zoom por rueda del mouse.
- Se reutilizó el mecanismo de `pendingTimelineZoomCenterRef` para mantener estable la línea temporal alrededor del punto actual de trabajo al mover el slider.

### Decisiones tomadas

- El zoom manual debe crecer o encogerse alrededor de la línea de tiempo activa, no desde un borde del viewport.

### Bloqueos o riesgos

- Si el playhead está muy cerca del inicio o del final, el scroll seguirá limitado por los bordes físicos disponibles de la timeline.

### Próximo paso

- Verificar manualmente que al mover el slider el playhead permanezca visualmente como ancla central del cambio de escala.

---

## 2026-03-22

### Trabajo realizado

- Se eliminó el `minTimelineZoom = 1` transitorio durante el arranque cuando la timeline todavía no conocía su ancho real.
- El clamp que empuja `timelineZoom` hacia el mínimo ahora solo corre cuando ya existen medidas válidas de viewport y duración.
- El slider de zoom pasó a reflejar el `timelineZoom` bruto persistido, no el `effectiveTimelineZoom` derivado temporalmente para render.

### Decisiones tomadas

- El primer render no debe reescribir el zoom del usuario con un mínimo provisional basado en información todavía incompleta del layout.

### Bloqueos o riesgos

- Si siguiera apareciendo un reset después de este cambio, ya no vendría del clamp inicial ni del valor mostrado por el slider; tocaría aislar una ruta posterior de reapertura o cambio de secuencia.

### Próximo paso

- Volver a probar refresh dejando el zoom en el extremo izquierdo y confirmar que el thumb y el ancho visible reaparecen exactamente igual.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la persistencia del zoom en refresh para guardar el `timelineZoom` bruto elegido por el usuario, en lugar del `effectiveTimelineZoom` calculado temporalmente durante el arranque.
- Esto evita que, mientras la timeline todavía no conoce su ancho real y el mínimo temporal cae en `1`, el editor reescriba por error el zoom guardado con ese valor transitorio.

### Decisiones tomadas

- El estado persistido debe reflejar la intención del usuario (`timelineZoom`), no una derivación momentánea de layout (`effectiveTimelineZoom`).

### Bloqueos o riesgos

- Si aparece otro overwrite posterior, ya no vendrá de esta capa de persistencia; habría que revisar una ruta puntual de reapertura o de selección de secuencia.

### Próximo paso

- Validar manualmente que mover el slider, refrescar y volver a entrar al mismo proyecto mantenga exactamente ese nivel de zoom.

---

## 2026-03-22

### Trabajo realizado

- Se cambió el modelo de zoom de timeline para permitir valores menores a `1`, eliminando el bloqueo que impedía alejar hasta ver la secuencia completa.
- El valor por defecto de `timelineZoom` quedó orientado a `fit-to-width`, de modo que el editor pueda mostrar todo el ancho útil de la secuencia sin exigir scroll lateral al abrirla.

### Decisiones tomadas

- `1` deja de ser el mínimo técnico del zoom; el mínimo real ahora puede bajar lo suficiente para acomodarse al ancho visible del panel.

### Bloqueos o riesgos

- El zoom guardado globalmente puede seguir reflejando una preferencia previa del usuario hasta que se vuelva a mover el slider en una sesión nueva.

### Próximo paso

- Verificar manualmente que el slider ya permita alejar por completo y que una secuencia larga entre entera en pantalla cuando así se desee.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la precedencia de hidratación del editor para que un draft local o backend vacío no tape secuencias recuperables.
- `hydrateProjectDraft` ahora prioriza primero drafts con secuencias reales; si no existen, intenta reconstruir una secuencia desde `clips` backend antes de aceptar un draft vacío.

### Decisiones tomadas

- Entre varias fuentes de estado, el editor debe preferir la fuente más rica en contenido, no simplemente la primera disponible.

### Bloqueos o riesgos

- Si un usuario esperaba conservar explícitamente un draft vacío a pesar de tener clips backend, este cambio priorizará la recuperación de contenido editable.

### Próximo paso

- Verificar que los proyectos que antes abrían sin secuencias vuelvan a mostrar al menos la secuencia recuperada desde clips cuando exista material suficiente.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió un error real de runtime en `DashboardPage.jsx`: `initialTimelineZoom` había quedado fuera del componente y referenciaba `initialLayoutPreferences` antes de existir.
- Ese fallo podía romper la carga del editor y falsear el comportamiento observado del zoom restaurado durante refresh.

### Decisiones tomadas

- La inicialización del zoom queda encapsulada dentro de `DashboardPage` para que dependa del estado hidratado correcto y no de una referencia fuera de scope.

### Bloqueos o riesgos

- Aunque el error de runtime ya quedó resuelto, todavía hace falta validación manual del flujo completo de refresh para confirmar si ese era el origen único del problema percibido.

### Próximo paso

- Reprobar refresh con el slider movido después de este fix, porque ahora sí el arranque debería reflejar fielmente lo guardado.

---

## 2026-03-22

### Trabajo realizado

- Se añadió una persistencia dedicada para `timelineZoom` en `localStorage`, separada del layout global y de la sesión general del editor.
- La timeline ahora inicializa su zoom prefiriendo ese valor dedicado y también lo reescribe inmediatamente cuando el usuario cambia el slider o hace zoom con rueda.
- `applyLayoutPreferences` se ajustó para no pisar el último zoom explícito del usuario con un valor de layout potencialmente viejo durante la carga.

### Decisiones tomadas

- El zoom de timeline pasa a tener una fuente de verdad propia para resistir rutas de carga múltiples o sobrescrituras al montar el editor.

### Bloqueos o riesgos

- Si en el futuro se quisiera un zoom distinto por proyecto o por secuencia, esta persistencia dedicada global habría que re-scopearla.

### Próximo paso

- Validar en navegador que después de mover el slider y refrescar, el thumb no vuelve a la posición genérica anterior.

---

## 2026-03-22

### Trabajo realizado

- Se reforzó la restauración del zoom de timeline durante refresh guardando también `timelineZoom` dentro de la sesión activa del editor, no solo en las preferencias globales.
- Al reabrir el proyecto, el valor de zoom de la sesión ahora se reaplica explícitamente después de hidratar el layout para evitar que otra ruta de carga lo deje en una posición genérica.

### Decisiones tomadas

- Para refresh del trabajo en curso, el zoom visible de la timeline se trata como estado de sesión exacto, aunque también siga existiendo como preferencia global de layout.

### Bloqueos o riesgos

- Si el usuario espera comportamientos distintos entre "abrir otro proyecto" y "refrescar el actual", puede hacer falta separar más claramente preferencias globales de estado de sesión.

### Próximo paso

- Validar en navegador que el thumb del slider y el ancho visible real de la timeline vuelven ambos al mismo estado exacto tras refresh.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la persistencia del `timelineZoom`: el layout global no se estaba recalculando cuando solo cambiaba el zoom, por lo que al refrescar se terminaba reusando un valor viejo o por defecto.
- La sesión del editor ahora guarda también la posición horizontal de la timeline (`scrollLeft`) y la restaura al reabrir el proyecto.
- El guardado de sesión del editor quedó con un debounce corto para evitar escrituras excesivas en `localStorage` mientras se hace scroll horizontal.

### Decisiones tomadas

- El nivel de zoom sigue siendo una preferencia global del editor.
- El tramo visible exacto de la timeline se restaura como estado de sesión, porque depende de la secuencia y del punto de trabajo actual.

### Bloqueos o riesgos

- Falta validar manualmente en navegador si el comportamiento deseado es restaurar el scroll exacto o recentrar automáticamente en el playhead en algunos flujos concretos.

### Próximo paso

- Probar refresh dejando la timeline en un zoom alto y desplazada a un punto específico para confirmar que vuelve exactamente al mismo tramo visible.

---

## 2026-03-22

### Trabajo realizado

- Se endureció la restauración de sesión al reabrir un proyecto desde refresh para que siempre resuelva una secuencia válida cuando exista al menos una en el draft.
- Se desacopló la restauración del chrome de sesión de la existencia inmediata de una secuencia resuelta, evitando que transcript, preview y timeline queden colapsados por una condición transitoria durante la reapertura.

### Decisiones tomadas

- El estado visual de la sesión debe restaurarse aunque el `activeSequenceId` guardado ya no coincida exactamente, siempre que el proyecto todavía tenga secuencias válidas.

### Bloqueos o riesgos

- Falta validar en navegador si todavía existe un caso de carrera entre el layout global y la restauración de sesión exacta durante el bootstrap del editor.

### Próximo paso

- Probar refresh completo con proyecto y secuencia activos para confirmar que el editor vuelve con el mismo panel expandido, la misma secuencia y el mismo playhead.

---

## 2026-03-22

### Trabajo realizado

- Se añadió un selector de modo visual por clip en la barra superior del panel `Preview`.
- El selector queda asociado al bloque activo o seleccionado en la timeline y ofrece `Fill`, `Fit` y `Split`.
- El modo elegido ahora se persiste dentro del draft editorial de la secuencia para preparar la siguiente etapa de ajustes visuales por clip.
- El preview ahora renderiza visualmente el clip seleccionado según su modo: `Fill` con cover, `Fit` con fondo blur y `Split` con duplicado automático según la orientación del canvas.
- Se añadió una cabecera flotante sobre el canvas para identificar el clip activo, el aspect ratio y el modo visible que se está editando.
- Al hacer click sobre la imagen del preview ahora aparece una capa de ajuste con handles para escalar proporcionalmente y arrastrar la imagen dentro del frame visible.
- Se añadió un botón flotante `Crop` que abre un modal oscuro con presets de relación de aspecto y un rectángulo de crop ajustable con handles sobre el frame actual del clip.
- Los ajustes de modo, posición, escala y crop ahora se persisten por clip dentro del draft editorial local.
- La sesión del editor ahora recuerda también el playhead y el bloque seleccionado al refrescar la página.
- El preview principal ahora re-sincroniza su `currentTime` al reabrir o refrescar para evitar que quede mostrando el primer frame hasta mover la timeline.
- Las preferencias de layout del editor pasaron a guardarse también como estado global: tamaños de módulos, colapsados, formato de preview y `timeline zoom` se conservan entre secuencias, proyectos y refresh.

### Decisiones tomadas

- El modo de visualización se guarda por bloque de secuencia, no como preferencia global del canvas.
- El control se colocó junto al selector de aspect ratio del preview para mantener el flujo de edición sobre el clip visible.

### Bloqueos o riesgos

- Todavía falta implementar el comportamiento visual real de `Fill`, `Fit` y `Split` en el canvas y sus modales de ajuste.
- En `Split` y `Fit` todavía falta exponer crop, escala y posición manual por panel o capa; por ahora se aplica solo el preset visual base.
- En `Split`, por ahora el overlay interactivo y el modal se aplican sobre el panel primario; falta separar completamente ajustes primario/secundario en la UI.

### Próximo paso

- Conectar este selector con el render del preview y luego con los controles flotantes de ajuste por clip.
- Añadir el botón flotante de ajuste y persistir transformaciones manuales por clip y por panel.
- Refinar el modal para soportar mejor flujos multi-panel en `Split` y presets visuales mas cercanos al ejemplo final.

---

## 2026-03-19

### Trabajo realizado

- El panel `Script principal` ahora tiene una altura regulable real desde UI (`Altura panel`) y deja de comportarse como una caja con recorte rígido: al crecer, empuja los módulos siguientes en vez de esconderlos dentro de la misma altura fija.
- El control general `Area` pasó a mostrarse como `Workbench` y ahora sí gobierna la altura efectiva del bloque editor + timeline.
- El panel derecho del `Script principal` recibió una grilla más estable: la terminal del agente y el texto de contexto tienen espacio dedicado y scroll propio, evitando que la parte superior quede cortada con facilidad.
- La barra de `Secuencias sugeridas` dejó de tener un borrado ambiguo: ahora hay un botón explícito para `Borrar seleccionada` y otro para `Borrar todas`, este último con confirmación.
- Se volvió a limpiar la base de desarrollo de sugeridas guardadas: había 1 registro de `AISuggestion` y quedó en 0.
- Se eliminaron los sliders de layout que estaban generando confusión y el ajuste de altura del `Script principal` pasó a hacerse con un divisor horizontal arrastrable, igual que en el editor de secuencia.
- Se reorganizó el espacio vertical del área central: `Secuencias creadas` dejó de vivir dentro de un scroll interno rígido y ahora se apoya más en el scroll general de la página cuando el `Script principal` crece.
- La `Terminal agente` ahora ocupa mejor el alto disponible en su columna, el aviso superior en modo agente se compactó y se añadió un botón `Copy code` para copiar el log visible al portapapeles.
- Se amplió el rango máximo de altura persistida para `Script principal`, eliminando el tope corto que impedía bajarlo más; al refrescar, la posición sigue restaurándose desde las preferencias globales guardadas.
- El resize de `Script principal` dejó de depender de una altura en `vh` y pasó a una altura persistida en píxeles aplicada al panel completo, lo que hace el arrastre más directo, libre y consistente al refrescar.
- El drag de `Script principal` se rehízo para medir la posición real del mouse dentro del contenedor con scroll y acompañar el autoscroll al arrastrar cerca del borde, evitando que el divisor se quede corto respecto al cursor.
- Se volvió a limpiar el lote activo de sugeridas en desarrollo tras un nuevo atasco del botón de borrado total: había 1 registro y quedó en 0.

### Decisiones tomadas

- La altura del `Script principal` no debe resolverse con un porcentaje abstracto del panel superior (`Superior`), sino con un control directo sobre la altura visible del propio módulo.
- Si un módulo necesita crecer para inspección, la UI debe permitir que desplace el contenido siguiente y use scroll de página antes que forzar recortes internos difíciles de leer.
- El borrado masivo de sugeridas debe ser una acción explícita y confirmada, separada del borrado puntual de una sola tarjeta.

### Bloqueos o riesgos

- Falta una validación manual fina en pantalla para ajustar si el rango `28vh..76vh` del panel superior se siente suficiente en monitores más bajos o si conviene ampliar todavía más el máximo.

### Próximo paso

- Probar manualmente en la vista principal que agrandar `Altura panel` permita inspeccionar la terminal sin recortes y que `Borrar todas` deje el módulo de sugeridas en cero desde la propia UI.

 - Se corrigió el cálculo del tramo textual del modal editorial para reconstruir la transcripción real del rango usando las palabras del candidato cuando sus timestamps no coincidían con el rango visible.
 - El modal editorial se volvió a replantear como comparación explícita: "qué había en el rango", "qué quitó / qué quedó" y "cómo se movieron los bloques", con botones y etiquetas renombrados a lenguaje editorial más claro.
- Se compactó el modal editorial para que no crezca en altura sin límite: ahora respeta la ventana, reduce la altura del preview y hace scroll interno por columna.
- Se corrigió un cruce de rangos en refine/adjust: el frontend estaba enviando el foco editorial equivocado al backend por la firma ampliada de buildDraftPayload; ahora cada depuración usa explícitamente los word_ids del candidato abierto.
- El preview editorial dejó de ser un video plano dentro del modal: ahora renderiza el modo visual del bloque activo (fill, fit o split) para que la revisión refleje también la decisión de layout propuesta por la IA.
- Se añadió un flujo para ampliar rango desde la depuración editorial: un botón abre un modal con el "universo" del rango por párrafos del transcript, permite expandir contexto anterior/posterior y opcionalmente fundir con sugeridas colindantes antes de pedir una nueva depuración.
- Las secuencias aprobadas desde una sugerida ampliada ahora conservan metadata de solape con otras sugeridas y se marcan en la barra de secuencias como compartidas para advertir que ese contexto quedó fusionado.
- Se acordó que la precisión final de in/out debe poder ajustarse manualmente.
- Se definió que el transcript es la superficie principal de edición.
- Se separó el concepto de rango de clip del estado editorial del texto.
- Se decidió permitir reordenamiento narrativo, incluso cuando el hook deba pasar al inicio.
- Se definió una UI de alta densidad inspirada en OBS.
- Se listaron librerías base para backend, frontend, waveform, drag and drop y procesamiento.
- Se creó la documentación base del proyecto.
- Se aclaró que el trim y el ajuste fino deben resolverse dentro de la app.
- Se definió que el XML a Premiere es una exportación final de interoperabilidad, no el lugar principal donde se completa la edición.
- Se acordó que la app debe leer automáticamente fps, resolución y metadata básica del video fuente.
- Se propuso usar una estrategia inicial de XML lineal estilo xmeml, pendiente de validación temprana con Premiere.
- Se añadió como objetivo del producto la exportación directa de una versión recortada del video desde la propia app.
- Se documentó como política provisional que la exportación de video tendrá un modo rápido, orientado a evitar re-encode cuando sea viable, y un modo final, orientado a fidelidad y precisión.
- Se creó el scaffold inicial completo del proyecto con backend Django y frontend React + Vite.
- Se añadió una página de settings para capturar API key, modelo y preferencias de exportación.
- Se creó una shell visual inicial del editor con transcript, preview, clips, inspector y timeline simulados.
- Se definieron modelos base para proyecto, video, transcript, secuencia, clips y anotaciones editoriales.
- Se añadieron endpoints base para health, overview, settings, proyectos y bootstrap del editor.
- Se generaron migraciones iniciales para `core` y `editor` y se aplicaron sobre SQLite.
- Se validó el frontend con build de producción.
- El editor no reporta errores de código en el estado actual del scaffold.
- Se implementó la creación real de proyectos vía multipart con subida de video y SRT.
- Se conectó `ffprobe` para extraer fps, resolución, duración y metadata básica de audio/video del archivo fuente.
- Se implementó el parseo de SRT y la persistencia de transcript y segmentos en base de datos.
- Se conectó el dashboard a la API para crear proyectos reales y mostrar el transcript persistido.
- Se validó el flujo end-to-end con un video de prueba generado por FFmpeg y un SRT de ejemplo.
- Se implementó la primera capa de sugerencias editoriales con endpoint dedicado por proyecto.
- Se dejó listo un contrato JSON de sugerencias con clips, orden y estados por segmento.
- Si no existe API key o la llamada a OpenAI falla, el sistema aplica un fallback heurístico local y sigue funcionando.
- Las sugerencias ya crean clips AI persistidos y anotaciones sobre los segmentos del transcript.
- Se validó end-to-end la generación de sugerencias sobre el proyecto de prueba.
- Se implementó la creación manual de clips desde segmentos seleccionados del transcript.
- Se añadió edición persistente de `label`, `in`, `out`, `orden` y borrado de clips con recálculo de secuencia.
- El preview ahora usa un reproductor real sobre el video fuente y ya permite reproducir clip y secuencia.
- Se implementó exportación XML estilo xmeml y exportación de video final vía FFmpeg.
- Se añadió el modelo `ExportJob` para persistir artefactos exportados.
- Se validó la generación real de XML y MP4 sobre el proyecto de prueba.
- Se verificó por `ffprobe` que el MP4 exportado mantiene resolución 1280x720, 25 fps y audio AAC sincronizado en la muestra.
- Se añadió un pipeline local para extraer audio del video y transcribirlo con timings palabra por palabra usando `faster-whisper`.
- Se añadieron modelos y endpoints para guardar transcript ASR local, palabras alineadas y archivo de audio extraído.
- Se corrigió el endpoint de transcripción para responder error de validación útil cuando no detecta voz, en lugar de devolver 500.
- Se validó end-to-end la transcripción local con un video sintético con voz, obteniendo segmentos precisos y 17 palabras persistidas con `start_ms` y `end_ms`.
- El bootstrap del editor ahora prioriza el transcript `asr-local` cuando existe y expone disponibilidad y conteo de word timings.
- El frontend ahora permite lanzar la transcripción local, ver el origen activo del transcript y navegar por palabras temporizadas.
- La creación de proyectos ya no requiere SRT: el usuario puede subir solo el MP4 y la app procesa el audio automáticamente.
- Se validó el flujo MP4-only creando un proyecto sin SRT y ejecutando extracción de audio + transcripción local de punta a punta.
- Se aclaró que la UI principal no debe abrir como dashboard de ingestión sino como panel de administración de proyectos ya procesados con botón `Add video`.
- Se redefinió el editor principal: transcript a la izquierda como texto corrido, preview al lado, y timeline + waveform abajo.
- Se aclaró que la unidad mínima real de edición es la palabra con `in` y `out` temporales internos, aunque esos tiempos no se muestren visualmente en el transcript.
- Se definió que borrar palabras o introducir cortes editoriales desde el texto debe recortar o dividir clips de video automáticamente.
- Se implementó una nueva pantalla principal con biblioteca de proyectos procesados y formulario mínimo `Add video`.
- Se añadió apertura por proyecto usando un endpoint de bootstrap dedicado por `project_id`.
- Se reemplazó la vista principal del editor por un transcript visual sin timestamps visibles, con palabras anulables y cortes editoriales locales por `Ctrl + click`.
- El preview y la línea de tiempo ahora derivan clips locales desde las palabras activas del transcript, en lugar de depender solo de clips persistidos.
- Se integró `stable-ts` como backend avanzado de transcripción local y se mantuvo `faster-whisper` como fallback opcional.
- Los settings de transcripción ahora quedan orientados por defecto a `stable-ts`, idioma `auto` y modelo `medium`.
- Se validó de punta a punta el pipeline nuevo sobre un MP4 corto del repositorio, persistiendo 3 segmentos y 17 palabras con timings.
- La retranscripción ya no bloquea el request HTTP: ahora corre en segundo plano con polling de estado y porcentaje visible en UI.
- Se añadió progreso persistido por proyecto y un endpoint de estado para evitar que la pantalla quede clavada en `Rehaciendo transcript...` sin feedback.
- El backend ahora lee también el `.env` raíz del workspace y puede tomar de ahí `OPENAI_API_KEY`, hosts y trusted origins del túnel.
- Se añadió una bitácora persistente por proyecto para requests de transcripción a OpenAI, con estado, payload enviado, respuesta resumida, preview textual y costo estimado.
- El editor ahora muestra un panel `OpenAI Trace` para inspeccionar cada request, el audio enviado por chunk y la estimación total de costo.
- Se corrigió un error de desarrollo local donde `DJANGO_USE_HTTPS=true` redirigía la API local a HTTPS y rompía el frontend con `ERR_SSL_PROTOCOL_ERROR`.
- La pantalla de biblioteca volvió al flujo acordado: `Add video` abre el selector de archivo y dispara el procesamiento automáticamente, sin selects manuales en el panel inferior.
- Se eliminó por completo el panel inferior redundante de `Add Video` y se añadió borrado de proyectos desde la lista, incluyendo limpieza de archivos asociados.
- Se corrigió el flujo de selección primaria para que `Generar secuencia` también funcione directamente desde un rango activo marcado con `shift + click`, aunque todavía no se haya pulsado `Agregar a primarias`.
- Se redujo ruido visual en el panel del transcript fuente: se retiraron banners de estado redundantes y se reemplazaron por un resumen compacto del rango actual, con contador de palabras y `in/out` del tramo seleccionado.
- El preview dejó de depender de los controles nativos del video fuente y pasó a usar transporte propio orientado a la secuencia manual, con salto forzado entre bloques activos y progreso expresado sobre la duración editada.
- Se eliminó la lista de tarjetas grandes de microclips en el preview y se reemplazó por navegación compacta por bloques de secuencia.
- La secuencia manual ya no queda solo en memoria del frontend: ahora se persiste en la secuencia activa del proyecto y se reconstruye al volver a abrirlo usando los clips guardados y los word timings del transcript.

### Decisiones tomadas

- El MVP es transcript-first.
- La IA debe sugerir estructura editorial, no cerrar el proyecto por sí sola.
- Debe existir corrección humana sobre texto, clips, orden e in/out.
- Se mantendrá trazabilidad entre transcript fuente, clip y secuencia editada.
- El usuario debe poder terminar la edición útil dentro de la app antes de exportar.
- Premiere queda como herramienta de respaldo o ajuste puntual, no como dependencia funcional del MVP.
- La app debe poder exportar tanto XML como un video final recortado.
- La promesa de calidad debe expresarse como calidad fuente o calidad equivalente, según el método de exportación necesario.
- Evitar recodificación se tratará como optimización deseable, no como garantía universal para cualquier secuencia editada.
- El proyecto arranca con frontend separado y backend API-first.
- El scaffold debe permitir comenzar inmediatamente la fase de ingesta y persistencia real.
- El entorno de trabajo local queda establecido con `.venv` en raíz del workspace y dependencias instaladas.
- La fase de ingesta básica queda resuelta y pasa a formar parte funcional del proyecto.
- La integración con IA queda iniciada y utilizable incluso sin credenciales gracias al fallback local.
- El editor ya permite una primera edición completa transcript-first desde ingesta hasta exportación.
- Cuando el SRT externo no sea suficientemente confiable, el transcript operativo debe pasar a ser el generado localmente desde el audio con timings por palabra.
- Los errores de transcripción sin segmentos deben tratarse como validación de dominio, no como fallo interno del servidor.
- El flujo operativo principal debe aceptar el MP4 de Restream como única entrada obligatoria; el SRT queda como mejora opcional.
- La experiencia del usuario debe empezar con administración de proyectos y pasar al editor solo después del procesamiento automático del video.
- Para audio real ruidoso o de directos, el backend operativo por defecto debe ser `stable-ts`, no `tiny` ni configuraciones ligeras.
- La key de OpenAI puede vivir en el `.env` raíz sin duplicarse en base de datos; la UI solo indica si existe y de dónde sale.
- OpenAI ya se está usando con timestamps por palabra y esos `start_ms` / `end_ms` quedan persistidos en `TranscriptWord`; no hace falta recalcular eso localmente después para el caso base.

### Bloqueos o riesgos

- Aún no está cerrado el formato XML exacto de exportación.
- La UI puede complejizarse rápido si se intenta construir todo a la vez.
- Habrá que diferenciar técnicamente cuándo una exportación puede ser copia directa y cuándo requiere re-encode.
- Queda pendiente validar con pruebas reales qué casos permiten mantener codec o bitrate originales sin sacrificar precisión útil.
- La llamada real a OpenAI queda pendiente de validación una vez se configure la API key.
- Falta validar la importación del XML directamente en Premiere.
- La waveform sigue siendo visual y todavía no usa análisis real de audio.
- Las interacciones avanzadas tipo Alt + click y solapamiento de clips aún no están implementadas.
- Falta unificar las sugerencias y anotaciones existentes con el transcript ASR local cuando un proyecto ya había sido editado usando SRT.
- Un modelo `medium` o `large-v3` en CPU seguirá siendo más lento; la mejora aplicada prioriza calidad de transcript sobre velocidad.
- El costo mostrado para `whisper-1` debe tratarse como estimado configurable por minuto, porque OpenAI puede mover pricing fuera del código del producto.
- La secuencia manual nueva aún vive en frontend; falta persistirla en backend para no perderla al recargar y para exportarla sin depender de reconstrucción local.

### Próximo paso

- Persistir en backend la secuencia manual creada desde selecciones primarias y conectar esa estructura con exportación XML y video.

---

## 2026-03-22

### Trabajo realizado

- Se corrigió la lógica de gaps y límites editoriales para que transcript y timeline representen la misma secuencia efectiva.
- Se separó el comportamiento de `C` y `Ctrl/Cmd + C`: el primero mantiene la lógica editorial cercana y el segundo fuerza corte exacto por tiempo.
- Se extendió el modelo editorial para soportar overrides exactos de inicio y fin de bloque, además de cortes exactos intermedios.
- Se refinó el trim en timeline para soportar scrub fino y ajustes exactos persistidos en el draft.
- Se reforzó la persistencia del draft editorial con recuperación desde backend y fallback desde clips cuando el draft local falta o es inválido.
- Se amplió la profundidad del historial persistido de undo/redo y quedó guardado dentro del draft del proyecto.
- Se corrigieron varios problemas de layout del timeline: anclaje inferior, dirección del resize, uso real del espacio vertical y scroll/viewport interno.
- Se ajustó la carga de waveform para evitar pérdida visual en el tramo final de la secuencia.
- Se añadió selector de canvas del preview con presets `16:9`, `9:16` y `1:1`.
- Se añadieron scripts de arranque rápido para Windows usando PowerShell como ruta principal de lanzamiento.

### Decisiones tomadas

- El modo exacto debe ser una excepción real al modelo transcript-first y no una simple variación del snap por palabra.
- El historial del editor debe persistirse junto con el draft para permitir volver muy atrás dentro del mismo proyecto.
- El preview puede cambiar de canvas independientemente del material fuente; la decisión pendiente es si eso debe trasladarse también a exportación.
- Para Windows, el launcher principal debe basarse en PowerShell y no en `cmd.exe` cuando haya problemas de apertura externa.

### Bloqueos o riesgos

- Falta verificar con uso real si el nuevo historial persistido necesita compresión o poda más inteligente.
- El canvas del preview ya cambia, pero aún no define por sí solo una política de exportación vertical o cuadrada.
- La validación real del XML en Premiere sigue pendiente.

### Próximo paso

- Convertir el preset de canvas en una decisión explícita de salida y validar qué parte del flujo de exportación debe respetarlo.