From 1186b242baf659cbf96c8f898a4f940beb9dc78f Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:23:06 +0700 Subject: [PATCH 01/18] timeline: Alt marquee selects clips without linked partners (#1163) --- crates/ui-egui/src/panels/timeline.rs | 15 ++++++----- crates/ui-egui/tests/timeline_marquee_ui.rs | 28 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 35a66ce3..65fe1516 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -162,6 +162,7 @@ pub enum Drag { start: Pos2, before: Vec, add: bool, + individual: bool, }, Divider, ZoomBar { @@ -2322,7 +2323,7 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & // the video and audio tracks, #683) (_, Grab::Other(hit @ (Hit::Empty { .. } | Hit::None))) if layout.content.contains(p) => { if resp.drag_started() { - Some(Drag::Marquee { start: p, before: app.session.state.selection.clone(), add: mods.shift || mods.command }) + Some(Drag::Marquee { start: p, before: app.session.state.selection.clone(), add: mods.shift || mods.command, individual: mods.alt }) } else { app.session.state.selection.clear(); app.session.state.edit_points.clear(); @@ -2446,20 +2447,22 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & Some(Drag::Transition { id, edge, from, start, duration }) } Drag::Volume(v) => Some(Drag::Volume(super::timeline_volume::drag(app, ui, seq, layout, v, p.y))), - Drag::Marquee { start, before, add } => { + Drag::Marquee { start, before, add, individual } => { let r = Rect::from_two_pos(start, p); ui.painter().rect_filled(r, 0.0, Color32::from_white_alpha(18)); ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_white_alpha(140)), StrokeKind::Inside); // the clips in the box are selected as it grows and shrinks; only the release runs // the command, so the drag is one selection change let mut sel = if add { before.clone() } else { Vec::new() }; - for c in filmcraft_engine::commands::with_links(&app.session, &marquee_clips(seq, layout, r)) { + let clips = marquee_clips(seq, layout, r); + let clips = if individual { clips } else { filmcraft_engine::commands::with_links(&app.session, &clips) }; + for c in clips { if !sel.contains(&c) { sel.push(c); } } app.session.state.selection = sel; - Some(Drag::Marquee { start, before, add }) + Some(Drag::Marquee { start, before, add, individual }) } other => Some(other), }; @@ -2508,13 +2511,13 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & let moved = seq.all_tracks().find_map(|tr| tr.transitions.iter().find(|x| x.id == id)).is_some_and(|x| (x.start, x.duration) != (start, duration)); moved.then(|| app.session.execute("sequence.setTransition", json!({"transition": id.0, "start": start.0, "duration": duration.0}))) } - Drag::Marquee { start, before, add } => { + Drag::Marquee { start, before, add, individual } => { app.session.state.selection = before; // egui ends a drag on Escape: that cancels the box, as in the Project panel match resp.interact_pointer_pos() { Some(end) if !ctx.input(|i| i.key_pressed(egui::Key::Escape)) => { let ids: Vec = marquee_clips(seq, layout, Rect::from_two_pos(start, end)).iter().map(|c| c.0).collect(); - Some(app.session.execute("timeline.select", json!({"clips": ids, "add": add}))) + Some(app.session.execute("timeline.select", json!({"clips": ids, "add": add, "linked": !individual}))) } _ => None, } diff --git a/crates/ui-egui/tests/timeline_marquee_ui.rs b/crates/ui-egui/tests/timeline_marquee_ui.rs index 962eefba..dc57ca6a 100644 --- a/crates/ui-egui/tests/timeline_marquee_ui.rs +++ b/crates/ui-egui/tests/timeline_marquee_ui.rs @@ -121,6 +121,34 @@ fn the_selection_follows_the_box_while_it_is_dragged() { assert!(d.selected(b) && !d.selected(a), "the release keeps what the box showed"); } + +#[test] +fn alt_box_selects_only_video_and_delete_keeps_linked_audio() { + let mut d = Driver::demo(); + let (_, video, _, rv, start) = d.setup(); + let audio = { + let seq = d.harness.state().session.active_sequence().unwrap(); + let link = seq.find_item(ClipId(video)).unwrap().1.link; + seq.audio_tracks.iter().flat_map(|tr| &tr.items).find(|clip| clip.link == link).unwrap().id.0 + }; + d.exec("timeline.select", json!({"clips": []})); + d.modifiers = egui::Modifiers::ALT; + d.send(egui::Event::PointerMoved(start)); + d.button(start, true); + let over = egui::pos2(rv.center().x, start.y + 2.0); + d.move_to(start, over); + assert!(d.selected(video)); + assert!(!d.selected(audio), "Alt excludes the linked audio during the drag"); + d.button(over, false); + d.modifiers = egui::Modifiers::NONE; + d.frames(2); + assert!(d.selected(video) && !d.selected(audio), "release preserves the individual selection"); + d.exec("edit.clear", json!({})); + let seq = d.harness.state().session.active_sequence().unwrap(); + assert!(seq.find_item(ClipId(video)).is_none()); + assert!(seq.find_item(ClipId(audio)).is_some(), "Delete leaves the unselected linked audio"); +} + #[test] fn shift_adds_to_the_selection_the_drag_began_with() { let mut d = Driver::demo(); From 369882123feec33943460658fcdc2da859bbcc03 Mon Sep 17 00:00:00 2001 From: Diego Ospina Date: Sun, 11 Oct 2026 23:08:20 -0500 Subject: [PATCH 02/18] i18n(es): fix mistranslations and terms, add 4 missing rows Scratch disks, sidecar files, burn-in, legal range, through edits, nudge units, butt cap and camera shake were mistranslated; render, view preset, transport and focus now match the rest of the catalog; four macOS menu and Preferences rows were missing. AI-assisted, native review welcome. Models: Opus 5.5, Terra 5.6, DeepSeek V4 Flash, Mistral Large 4 --- crates/ui-egui/src/i18n/es.tsv | 80 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 25a23480..8f849fed 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -293,7 +293,7 @@ Save as Template… Guardar como plantilla… Scale to Frame Size Escalar al tamaño del fotograma Scene Edit Detection… Detección de cambios de escena… - Scratch Disks… Discos de memoria virtual… + Scratch Disks… Discos de trabajo… Search Bin Bandeja de búsqueda Select Seleccionar Select All Seleccionar todo @@ -317,7 +317,7 @@ Show All Marker Colors Mostrar todos los colores de marcador Show Guides Mostrar guías Show Rulers Mostrar reglas - Show Through Edits Mostrar ediciones pasantes + Show Through Edits Mostrar cortes continuos Simplify Sequence… Simplificar secuencia… Snap in Program Monitor Ajustar en el monitor de programa Snap in Timeline Ajustar en la línea de tiempo @@ -341,7 +341,7 @@ Transcribe… Transcribir… Transcript Transcripción Transparent Video… Video transparente… - Trim Edit Recortar edición + Trim Edit Edición de recorte Trim… Recorte… Undo Deshacer Ungroup Desagrupar @@ -543,8 +543,8 @@ Search effects Buscar efectos Apply to Selected Clips Aplicar a los clips seleccionados Metadata Display Visualización de metadatos - Save As New View Preset Guardar como nuevo ajuste de vista - Manage Saved View Presets Gestionar ajustes de vista guardados + Save As New View Preset Guardar como nuevo ajuste preestablecido de vista + Manage Saved View Presets Gestionar ajustes preestablecidos de vista guardados Freeform View Options Opciones de la vista de forma libre Save Arrangement Guardar disposición Saves the view, columns, sort, thumbnail and font size. Guarda la vista, las columnas, el orden, la miniatura y el tamaño de fuente. @@ -597,12 +597,12 @@ Out: {name} Salida: {name} In: {name} Entrada: {name} Play Around Edit (Space loops, Shift+K once) Reproducir alrededor de la edición (Espacio en bucle, Mayús+K una vez) - Trim Backward Many Recortar muchos hacia atrás + Trim Backward Many Recortar varios fotogramas hacia atrás Trim Backward Recortar hacia atrás Apply Default Transitions Aplicar transiciones predeterminadas Apply Default Transitions to Selection (Shift+D) Aplicar transiciones predeterminadas a la selección (Mayús+D) Trim Forward Recortar hacia delante - Trim Forward Many Recortar muchos hacia delante + Trim Forward Many Recortar varios fotogramas hacia delante Exit Trim Mode Salir del modo de recorte Voice-over record (right-click: Voice-Over Record Settings…) Grabar voz en off (clic derecho: ajustes de grabación de voz en off…) Voice-Over Record Settings… Ajustes de grabación de voz en off… @@ -677,7 +677,7 @@ Extract (') Extraer (') Export Frame: use Export mode (M6) Exportar fotograma: usa el modo Exportar (M6) Button Editor Editor de botones - More transport controls Más controles de reproducción + More transport controls Más controles de transporte Multi-Camera Record on Grabación multicámara activada Multi-Camera Record off Grabación multicámara desactivada {first} - Merged {first} - Combinado @@ -710,7 +710,7 @@ Enumerate Cameras Numerar cámaras Camera Angle Metadata Metadatos de ángulo de cámara Move source clips to Processed Clips bin Mover los clips de origen a la bandeja Clips procesados - Synchronized {n} clip group(s) Sincronizado(s) {n} grupo(s) de clips + Synchronized {n} clip group(s) {n} grupo(s) de clips sincronizado(s) Merged clip created Clip combinado creado Multi-camera source sequence with {n} camera(s) Secuencia de origen multicámara con {n} cámara(s) No multi-camera clip at the playhead No hay ningún clip multicámara en el cabezal @@ -722,7 +722,7 @@ Record Grabar Edit Cameras Editar cámaras Cameras shown in the Multi-Camera view (uncheck to hide). Cámaras mostradas en la vista multicámara (desmarca para ocultar). - Recorded {n} multi-camera cut(s) Grabado(s) {n} corte(s) multicámara + Recorded {n} multi-camera cut(s) {n} corte(s) multicámara grabado(s) Clamp Signal Limitar señal Presets Ajustes preestablecidos Parade ({kind}) Desfile ({kind}) @@ -794,7 +794,7 @@ Offline Sin conexión Missing No encontrado Link Media Vincular medios - {n} clip(s) can't find their media. Locate them, search a folder, or leave them offline. {n} clip(s) no encuentran sus medios. Localízalos, busca en una carpeta o déjalos sin conexión. + {n} clip(s) can't find their media. Locate them, search a folder, or leave them offline. {n} clip(s) no encuentra(n) sus medios. Localízalo(s), busca en una carpeta o déjalo(s) sin conexión. Clip Name Nombre del clip File Name Nombre de archivo File Path Ruta del archivo @@ -1192,9 +1192,9 @@ Thumbnail controls for all pointing devices Controles de miniaturas para todos los dispositivos señaladores Refresh Sort Order Actualizar el orden Metadata Display… Visualización de metadatos… - Save Current View Preset Guardar el ajuste de vista actual - Restore View Preset Restaurar ajuste de vista - Project View Preset {n} Ajuste de vista del proyecto {n} + Save Current View Preset Guardar el ajuste preestablecido de vista actual + Restore View Preset Restaurar ajuste preestablecido de vista + Project View Preset {n} Ajuste preestablecido de vista del proyecto {n} Freeform View Options… Opciones de la vista de forma libre… Media missing Medio no encontrado Unreadable Ilegible @@ -1375,7 +1375,7 @@ Black and white Blanco y negro Black and white with crushed blacks and strong contrast Blanco y negro con negros aplastados y contraste fuerte Bleach Bypass Blanqueo omitido - Bleach-bypass style: desaturated with hard contrast Estilo blanqueo omitido: desaturado y con contraste duro + Bleach-bypass style: desaturated with hard contrast Estilo de blanqueo omitido: desaturado y con contraste duro Blend Mezclar Blend Mode Modo de fusión Blend With Original Mezclar con el original @@ -1418,7 +1418,7 @@ Bright Air Aire brillante Bright Studio Estudio brillante Brightness & Contrast Brillo y contraste - British Stack Pila británica + British Stack Stack británico Brush Hardness Dureza del pincel Brush Opacity Opacidad del pincel Brush Position Posición del pincel @@ -1432,14 +1432,14 @@ Burn Alpha Quemar alfa Burn Chroma Quemar croma Burn Color Quemar color - Butt cap Extremo cuadrado + Butt cap Extremo plano Butterworth Butterworth CEA-608 CEA-608 CEA-708 CEA-708 Cache analysis results for re-use Guardar en caché los resultados del análisis para reutilizarlos Camera Angle Ángulo de cámara Camera Blur Desenfoque de cámara - Camera Shake Movimiento de cámara + Camera Shake Vibración de cámara Capitalisation Uso de mayúsculas Cathedral Catedral Cell Pattern Patrón de celdas @@ -1724,7 +1724,7 @@ Flip Motion Movimiento de volteo Flip Over Voltear Float Coma flotante - Focus Foco + Focus Enfoque Focus Blur Desenfoque de foco Focus Point Punto de enfoque Focus Shape Forma del foco @@ -1826,12 +1826,15 @@ Hard Limiter Limitador duro Hard Mix Mezcla definida Hardware decoding Descodificación por hardware + Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. Descodificación por hardware: Automático usa el descodificador de video del sistema (VideoToolbox en macOS, Media Foundation en Windows, VA-API para H.264 y HEVC en Linux) con los flujos que admite, y el descodificador propio de FilmCraft para todo lo demás o si falla el hardware. Los medios ya abiertos conservan su descodificador hasta que se vuelven a abrir. Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API or NVDEC for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. Descodificación por hardware: Automático usa el descodificador de video del sistema (VideoToolbox en macOS, Media Foundation en Windows, VA-API o NVDEC para H.264 y HEVC en Linux) para los flujos que admite, y el descodificador propio de FilmCraft para todo lo demás o si falla el hardware. Los medios ya abiertos mantienen su descodificador hasta que se vuelven a abrir. Head Size Tamaño de la cabeza Heart Corazón Height Altura Hexagon Hexágono Hide Back Face Ocultar la cara posterior + Hide FilmCraft Ocultar FilmCraft + Hide Others Ocultar otras High Alta High Cutoff Corte de agudos High Frequency Cut Corte de altas frecuencias @@ -1908,7 +1911,7 @@ Italian Italiano Japanese Japonés JetBrains Mono JetBrains Mono - Join Through Edits Unir ediciones pasantes + Join Through Edits Unir cortes continuos Ripple Trim In Recorte de rizo de entrada Ripple Trim Out Recorte de rizo de salida Roll Edit Edición de rodillo @@ -2047,7 +2050,7 @@ Median Mediana Medium Room Sala mediana Memory Memoria - Metadata & Timecode Burn-in Impresión de metadatos y código de tiempo + Metadata & Timecode Burn-in Incrustación de metadatos y código de tiempo Method Método Metrics Kerning Ajuste entre pares métrico Mid Medios @@ -2338,7 +2341,7 @@ Scan Lines Líneas de exploración Scene Escena Scientific Filter Filtro científico - Scratch disk location for Captured Audio Ubicación del disco de memoria virtual para el audio capturado + Scratch disk location for Captured Audio Ubicación del disco de trabajo para el audio capturado Screen Trama Seconds Segundos Segments Segmentos @@ -2348,6 +2351,7 @@ Set focus on the Timeline when performing Insert/Overwrite edits Enfocar la línea de tiempo al insertar o sobrescribir Set to frame size Ajustar al tamaño del fotograma Setting Ajuste + Settings Ajustes Shadow Angle Ángulo de la sombra Shadow Blue Balance Balance de azul en sombras Shadow Blur Desenfoque de la sombra @@ -2753,7 +2757,7 @@ All Todos Any Cualquiera Case Sensitive Distinguir mayúsculas y minúsculas - Scratch Disks Discos de memoria virtual + Scratch Disks Discos de trabajo Ingest Settings Ajustes de ingesta Captured and Generated: Capturado y generado: Video Previews: Previsualizaciones de video: @@ -2891,7 +2895,7 @@ The sequence has no caption track. La secuencia no tiene pista de subtítulos. Export Options Opciones de exportación Burn Captions Into Video Grabar los subtítulos en el video - Create Sidecar File Crear archivo adjunto + Create Sidecar File Crear archivo complementario File Format Formato de archivo SubRip Subtitle (.srt) Subtítulo SubRip (.srt) WebVTT (.vtt) WebVTT (.vtt) @@ -2954,7 +2958,7 @@ File Name & Location Nombre y ubicación del archivo Quick Export: {path} Exportación rápida: {path} Graphics problem: switched to software compositing Problema de gráficos: se ha cambiado a la composición por software - GPU acceleration is unavailable; preview renders on the CPU La aceleración por GPU no está disponible; la vista previa se renderiza en la CPU + GPU acceleration is unavailable; preview renders on the CPU La aceleración por GPU no está disponible; la vista previa se procesa en la CPU Exporting Exportando Click to select, or click in empty space and drag to marquee select. Use Shift, Opt, and Cmd for other options. Haz clic para seleccionar, o haz clic en un espacio vacío y arrastra para seleccionar con un marco. Usa Mayús, Opción y Cmd para otras opciones. Click to select all clips to the right in all tracks. Shift-click for a single track. Haz clic para seleccionar todos los clips a la derecha en todas las pistas. Mayús+clic para una sola pista. @@ -2974,7 +2978,7 @@ (empty) (vacío) Step Back 1 Frame Retroceder 1 fotograma Step Forward 1 Frame Avanzar 1 fotograma - Match Playhead Igualar cabezal + Match Playhead Igualar al cabezal Gang to Program Vincular al programa AAC Audio AAC Audio AAC Audio 48 kHz 320 kbps AAC Audio 48 kHz 320 kbps @@ -3171,14 +3175,14 @@ Nudge Clip Selection Right Five Frames Desplazar la selección de clips cinco fotogramas a la derecha Nudge Clip Selection Right One Frame Desplazar la selección de clips un fotograma a la derecha Nudge Clip Selection Up Desplazar la selección de clips hacia arriba - Nudge Selected Object down by five Desplazar el objeto seleccionado cinco hacia abajo - Nudge Selected Object down by one Desplazar el objeto seleccionado uno hacia abajo - Nudge Selected Object to left by five Desplazar el objeto seleccionado cinco a la izquierda - Nudge Selected Object to left by one Desplazar el objeto seleccionado uno a la izquierda - Nudge Selected Object to right by five Desplazar el objeto seleccionado cinco a la derecha - Nudge Selected Object to right by one Desplazar el objeto seleccionado uno a la derecha - Nudge Selected Object up by five Desplazar el objeto seleccionado cinco hacia arriba - Nudge Selected Object up by one Desplazar el objeto seleccionado uno hacia arriba + Nudge Selected Object down by five Desplazar el objeto seleccionado cinco píxeles hacia abajo + Nudge Selected Object down by one Desplazar el objeto seleccionado un píxel hacia abajo + Nudge Selected Object to left by five Desplazar el objeto seleccionado cinco píxeles a la izquierda + Nudge Selected Object to left by one Desplazar el objeto seleccionado un píxel a la izquierda + Nudge Selected Object to right by five Desplazar el objeto seleccionado cinco píxeles a la derecha + Nudge Selected Object to right by one Desplazar el objeto seleccionado un píxel a la derecha + Nudge Selected Object up by five Desplazar el objeto seleccionado cinco píxeles hacia arriba + Nudge Selected Object up by one Desplazar el objeto seleccionado un píxel hacia arriba Nudge Volume +1dB Ajustar volumen +1 dB Nudge Volume +3dB Ajustar volumen +3 dB Nudge Volume -1dB Ajustar volumen -1 dB @@ -3387,7 +3391,7 @@ Protect Highlights Proteger luces Half Saturation Saturación al 50 % Boost Vibrance Aumentar intensidad - Legal Range Squeeze Compresión al rango de vídeo + Legal Range Squeeze Compresión al rango legal A boxed label with a round pointer; move the label and the pointer follows. Una etiqueta enmarcada con un indicador redondo; al mover la etiqueta, el indicador la sigue. A rounded tag that fits its text, with a status dot. Una etiqueta redondeada que se ajusta al texto, con un punto de estado. A title on a rounded card with a soft shadow. Un título en una tarjeta redondeada con una sombra suave. @@ -3435,7 +3439,7 @@ Show Mostrar Sort Ordenar Reverse the order Invertir el orden - Grab bag Mosaico + Grab bag Miscelánea Table Tabla No contributor data was built into this copy. Esta copia no incluye datos de colaboradores. No model credits were built into this copy. Esta copia no incluye créditos de modelos. @@ -3556,7 +3560,7 @@ Gradient Angle Ángulo del degradado Linear Gradient Degradado lineal Solid Sólido - GPU Rendering Renderizado por GPU + GPU Rendering Procesamiento por GPU Composite on the GPU when available Componer en la GPU cuando esté disponible Text to Speech Texto a voz Edit Narration Editar narración @@ -3584,7 +3588,7 @@ Use Default Graphics Usar gráficos predeterminados The default graphics are used from the next start. Los gráficos predeterminados se usarán a partir del próximo inicio. DirectX 12 only Solo DirectX 12 - software rendering renderizado por software + software rendering procesamiento por software Label Color Presets Ajustes preestablecidos de color de etiqueta {name} (Modified) {name} (modificado) Revert to the preset's colors and names Volver a los colores y nombres del ajuste preestablecido From 25aeb4d8a46290bf8b2a5015ba55c22b977f6449 Mon Sep 17 00:00:00 2001 From: gyro Date: Mon, 12 Oct 2026 00:41:04 -0700 Subject: [PATCH 03/18] Fix clip drags leaving transitions behind Dragging a clip kept its transitions on their old spots, so they jumped to the dropped position on release instead of moving with the clip. The drag preview now draws each transition where the drop will leave it, applying the same rule the edit does: a fade travels with its clip, and a cut transition travels only when every clip it sits on moves to one track by one offset. A cut transition whose one side stays behind is dropped on release; the preview keeps it on its cut while dragging, and Option-drag copies show a ghost transition beside the copies, duplicating only the transitions whose every clip is copied. - edit: carried_transitions keeps only all-clips-move transitions; move_items and copy_items drop or skip the rest - ui: move_transition_preview draws carried transitions where they land - tests: timeline_transition_move_ui (fade, both-clips cut, XYZ partial drop, cross-track drop, Option-drag ghost); transitions_travel_with_moved_clips updated to the drop-on-partial rule --- crates/edit/src/lib.rs | 21 +- crates/edit/src/tests.rs | 2 +- crates/ui-egui/src/panels/timeline.rs | 71 +++++- .../tests/timeline_transition_move_ui.rs | 238 ++++++++++++++++++ 4 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 crates/ui-egui/tests/timeline_transition_move_ui.rs diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 17306f28..2f2588d6 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -559,7 +559,8 @@ pub fn copy_items(seq: &mut Sequence, moves: &[(ClipId, TrackId, Tick)], insert_ it.start = (*start).max(Tick::ZERO); placed.push((*dest, it)); } - // worked out on the original ids, then moved over to the copies + // worked out on the original ids, then moved over to the copies (only transitions whose every + // clip is copied travel; one whose partner stays with the originals is not duplicated) let carried = carried_transitions(&work, &placed); let mut ids = HashMap::new(); let mut links = HashMap::new(); @@ -596,9 +597,10 @@ pub fn copy_items(seq: &mut Sequence, moves: &[(ClipId, TrackId, Tick)], insert_ Ok(out) } -/// The transitions that travel with a move (#227, #374): those whose clips all move, onto one -/// track and by one offset (a fade at a moved clip's edge, or the cut between two clips that move -/// together), shifted to where those clips land. +/// The transitions that travel with a move (#227, #374): only those whose every clip moves, onto +/// one track and by one offset (a fade at a moved clip's edge, or the cut between two clips that +/// move together), shifted to where those clips land. A transition whose partner stays behind is +/// dropped, as in Premiere (the drag preview leaves it where it is until the release). fn carried_transitions(seq: &Sequence, placed: &[(TrackId, TrackItem)]) -> Vec<(TrackId, Transition)> { let landing = |c: ClipId| { let (dest, it) = placed.iter().find(|(_, it)| it.id == c)?; @@ -607,9 +609,14 @@ fn carried_transitions(seq: &Sequence, placed: &[(TrackId, TrackItem)]) -> Vec<( }; let mut carried = Vec::new(); for tr in seq.all_tracks().filter(|t| !t.locked).flat_map(|t| t.transitions.iter()) { - let lands: Option> = [tr.from, tr.to].into_iter().flatten().map(landing).collect(); - let Some((&(dest, delta), rest)) = lands.as_deref().and_then(<[_]>::split_first) else { continue }; - if rest.iter().all(|l| *l == (dest, delta)) { + let endpoints: Vec = [tr.from, tr.to].into_iter().flatten().collect(); + let lands: Vec<(TrackId, Tick)> = endpoints.iter().filter_map(|c| landing(*c)).collect(); + // every clip it sits on must move, onto one track by one offset, or the transition is + // dropped with the delete below (a partner that stayed behind ends the move) + if lands.len() == endpoints.len() + && let Some(&(dest, delta)) = lands.first() + && lands.iter().skip(1).all(|l| *l == (dest, delta)) + { carried.push((dest, Transition { start: tr.start + delta, ..tr.clone() })); } } diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index dd054cdf..9b42c106 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -554,7 +554,7 @@ fn transitions_travel_with_moved_clips() { // a cut whose two clips move together keeps its transition, in an insert move as well move_items(&mut fx.seq, &[(b, v1, f(60)), (c, v1, f(70))], true, &mut Fx::ctx(&mut n)).unwrap(); assert_eq!(at(&fx, v1), vec![(2, 68), (3, 76)]); - // a crossfade whose partner stays behind is dropped; the other clip's fade stays with it + // a crossfade whose partner stays behind is dropped; the moving clip's own fade stays with it move_items(&mut fx.seq, &[(c, v1, f(90))], false, &mut Fx::ctx(&mut n)).unwrap(); assert_eq!(at(&fx, v1), vec![(3, 96)]); fx.seq.check().unwrap(); diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index ac4caf53..1178edac 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -558,22 +558,39 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { } // selected edit points (trim mode): red brackets for ripple, yellow for roll / regular trim super::trim_monitor::paint_edit_points(app, &p, seq, tr, r, &layout); - // transitions (one being dragged where the drag puts it; the selected ones outlined) + // transitions (one being dragged where the drag puts it; the selected ones outlined). A + // clip drag also shows where the drop will carry its transitions (those whose clips all + // move to one track by one offset): a plain move draws them on their destination track + // right away, a copy adds a ghost beside the copies while the originals stay at home. for trn in &tr.transitions { + let carried = move_transition_preview(app, seq, trn); + // the transition's home span (a transition dragged by an end still takes over) let (start, end) = match &app.tl.drag { Some(Drag::Transition { id, start, duration, .. }) if *id == trn.id => (*start, *start + *duration), _ => (trn.start, trn.end()), }; - let x0 = layout.x_of(start); - let x1 = layout.x_of(end); - let tr_rect = Rect::from_min_max(pos2(x0, r.rect.min.y + 17.0), pos2(x1, r.rect.max.y - 1.0)); - let selected = app.session.state.transition_selection.contains(&trn.id); - draw_transition(&p, tr_rect, trn, selected, &t); - app.auto.add(&format!("timeline.transition.{}", trn.id.0), tr_rect, &trn.effect.effect); - // its ends, for dragging the duration (the same zones as the hit test) - let zone = super::timeline_hit::EDGE_PX.min(tr_rect.width() / 3.0); - app.auto.add(&format!("timeline.transition.{}.in", trn.id.0), Rect::from_min_max(tr_rect.min, pos2(x0 + zone, tr_rect.max.y)), "start"); - app.auto.add(&format!("timeline.transition.{}.out", trn.id.0), Rect::from_min_max(pos2(x1 - zone, tr_rect.min.y), tr_rect.max), "end"); + // (start, end, is the copy's ghost) + let show: Vec<(Tick, Tick, bool)> = match (carried, copying) { + (Some((s, e, dest)), false) if dest == r.track => vec![(s, e, false)], + (Some((s, e, dest)), true) if dest == r.track => vec![(start, end, false), (s, e, true)], + (Some(_), false) => Vec::new(), // drawn on its destination row + _ => vec![(start, end, false)], // at home: not carried, or a copy's original + }; + for (start, end, ghost) in show { + let x0 = layout.x_of(start); + let x1 = layout.x_of(end); + let tr_rect = Rect::from_min_max(pos2(x0, r.rect.min.y + 17.0), pos2(x1, r.rect.max.y - 1.0)); + let selected = app.session.state.transition_selection.contains(&trn.id); + draw_transition(&p, tr_rect, trn, ghost || selected, &t); + let suffix = if ghost { ".copy" } else { "" }; + app.auto.add(&format!("timeline.transition.{}{}", trn.id.0, suffix), tr_rect, &trn.effect.effect); + // its ends, for dragging the duration (the same zones as the hit test) + let zone = super::timeline_hit::EDGE_PX.min(tr_rect.width() / 3.0); + if !ghost { + app.auto.add(&format!("timeline.transition.{}.in", trn.id.0), Rect::from_min_max(tr_rect.min, pos2(x0 + zone, tr_rect.max.y)), "start"); + app.auto.add(&format!("timeline.transition.{}.out", trn.id.0), Rect::from_min_max(pos2(x1 - zone, tr_rect.min.y), tr_rect.max), "end"); + } + } } } @@ -1837,6 +1854,38 @@ fn preview_drag(app: &FilmcraftApp, seq: &Sequence, out: &mut HashMap Option<(Tick, Tick, TrackId)> { + let Some(Drag::Move { clips, offset, track_delta, .. }) = &app.tl.drag else { return None }; + let endpoints: Vec = [trn.from, trn.to].into_iter().flatten().collect(); + // where each clip it sits on that moves lands (the same track shift as the clip preview), and + // by how much: clamped like the drop clamps each clip's start + let lands: Vec<(TrackId, Tick)> = endpoints + .iter() + .filter_map(|c| { + if !clips.contains(c) { + return None; + } + let (tid, it) = seq.find_item(*c)?; + let dest = shift_track(seq, tid, *track_delta)?; + Some((dest, (it.start + *offset).max(Tick::ZERO) - it.start)) + }) + .collect(); + // every clip it sits on must move to one track by one offset; one staying behind keeps the + // transition where it is (and the drop does not carry it either) + if lands.len() != endpoints.len() { + return None; + } + let (dest, delta) = lands[0]; + lands.iter().skip(1).all(|l| *l == (dest, delta)).then(|| (trn.start + delta, trn.end() + delta, dest)) +} + /// The clips a marquee rectangle touches, on the tracks it crosses. fn marquee_clips(seq: &Sequence, layout: &Layout, r: Rect) -> Vec { let (a, b) = (layout.tick_at(r.min.x), layout.tick_at(r.max.x)); diff --git a/crates/ui-egui/tests/timeline_transition_move_ui.rs b/crates/ui-egui/tests/timeline_transition_move_ui.rs new file mode 100644 index 00000000..4c60ae64 --- /dev/null +++ b/crates/ui-egui/tests/timeline_transition_move_ui.rs @@ -0,0 +1,238 @@ +//! Dragging clips previews the transitions the drop will carry, exactly as the drop re-adds them +//! (`edit::move_items`): a fade follows its clip, a cut transition travels with its clips only +//! when every one of them moves — dragging a clip out from under one side of a cut leaves the +//! transition where it was until the release deletes it (Premiere) — an Option-drag adds a +//! traveling copy while the original stays. + +use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2}; +use egui_kittest::Harness; +use filmcraft_engine::Session; +use filmcraft_ui_egui::FilmcraftApp; +use serde_json::json; + +fn step(h: &mut Harness<'static, FilmcraftApp>) { + let ctx = h.ctx.clone(); + let mut raw = std::mem::take(h.input_mut()); + eframe::App::raw_input_hook(h.state_mut(), &ctx, &mut raw); + *h.input_mut() = raw; + ctx.request_repaint(); + h.step(); +} + +fn pointer(h: &mut Harness<'static, FilmcraftApp>, pos: Pos2, pressed: Option) { + h.input_mut().events.push(Event::PointerMoved(pos)); + if let Some(pressed) = pressed { + h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }); + } + step(h); +} + +fn rect(h: &Harness<'static, FilmcraftApp>, id: &str) -> Rect { + let [x, y, w, height] = h.state().auto.elements.iter().find(|e| e.id == id).unwrap_or_else(|| panic!("missing {id}")).rect; + Rect::from_min_size(pos2(x, y), vec2(w, height)) +} + +fn build(s: Session) -> Harness<'static, FilmcraftApp> { + let app = FilmcraftApp::new(s); + let mut builder = Harness::builder().with_size(vec2(1600.0, 980.0)).with_max_steps(10_000); + if std::env::var_os("FILMCRAFT_UI_SNAPSHOT_DIR").is_some() { + builder = builder.wgpu(); + } + let mut h = builder.build_eframe(move |_cc| app); + for _ in 0..4 { + step(&mut h); + } + // Opening the new sequence restores its own view; set the test zoom after that initial fit. + h.state_mut().ui.timeline.pps = 60.0; + h.state_mut().ui.timeline.target_pps = 60.0; + h.state_mut().ui.timeline.fit_pending = false; + step(&mut h); + h +} + +/// A sequence with a cross dissolve: between two clips at 0..48f and 48..96f, or a fade-in on the +/// single clip at 0..48f when `single`. +fn harness(single: bool) -> Harness<'static, FilmcraftApp> { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + s.execute("file.newSequence", json!({"name":"Transition move","fps":24})).unwrap(); + let rate = s.sequence_rate(); + for (at, frames) in if single { vec![(0, 48)] } else { vec![(0, 48), (48, 48)] } { + s.execute("timeline.place", json!({"item":5,"time":rate.tick_of(at).0,"duration":rate.tick_of(frames).0})).unwrap(); + } + let seq = s.active_sequence().unwrap().clone(); + let clip = seq.video_tracks[0].items[if single { 0 } else { 1 }].id; + s.execute("effects.apply", json!({"effect": "cross_dissolve", "clip": clip.0, "edge": "in"})).unwrap(); + build(s) +} + +/// XYZ with a cut transition on each cut: X↔Y and Y↔Z. +fn harness_xyz() -> Harness<'static, FilmcraftApp> { + let mut s = Session::default(); + s.execute("file.openDemoProject", json!({})).unwrap(); + s.execute("file.newSequence", json!({"name":"Transition move","fps":24})).unwrap(); + let rate = s.sequence_rate(); + for (at, frames) in [(0, 48), (48, 48), (96, 48)] { + s.execute("timeline.place", json!({"item":5,"time":rate.tick_of(at).0,"duration":rate.tick_of(frames).0})).unwrap(); + } + let seq = s.active_sequence().unwrap().clone(); + let (y, z) = (seq.video_tracks[0].items[1].id, seq.video_tracks[0].items[2].id); + for clip in [y, z] { + s.execute("effects.apply", json!({"effect": "cross_dissolve", "clip": clip.0, "edge": "in"})).unwrap(); + } + build(s) +} + +/// A drag preview that keeps a transition's offset to its clip (it follows, not lags), and a drop +/// that re-adds the transition shifted by exactly the move. +#[test] +fn dragging_a_clip_carries_its_fade_in_the_preview_and_on_the_drop() { + let mut h = harness(true); + let q0 = h.state().session.active_sequence().unwrap().clone(); + let (a0, tr0) = (q0.video_tracks[0].items[0].clone(), q0.video_tracks[0].transitions[0].clone()); + let a_rect = rect(&h, &format!("timeline.clip.{}", a0.id.0)); + let tr_rect = rect(&h, &format!("timeline.transition.{}", tr0.id.0)); + assert_eq!(tr0.to, Some(a0.id), "the fade-in sits on the dragged clip"); + let gap_before = tr_rect.min.x - a_rect.min.x; + let y = a_rect.center().y; + // press clear of the fade band (it covers the left 75 px of the clip) and of the edge zones + let x0 = a_rect.left() + 110.0; + pointer(&mut h, pos2(x0, y), Some(true)); + for i in 1..=6 { + pointer(&mut h, pos2(x0 + 20.0 * i as f32, y), None); + } + // mid-drag: the fade follows the clip's ghost, keeping its offset to the clip edge + let gap_mid = rect(&h, &format!("timeline.transition.{}", tr0.id.0)).min.x - rect(&h, &format!("timeline.clip.{}", a0.id.0)).min.x; + assert!((gap_mid - gap_before).abs() < 1.0, "fade lagged behind: gap {gap_before} -> {gap_mid}"); + pointer(&mut h, pos2(x0 + 140.0, y), Some(false)); + let q1 = h.state().session.active_sequence().unwrap().clone(); + let (a1, tr1) = (q1.video_tracks[0].items[0].clone(), q1.video_tracks[0].transitions[0].clone()); + assert_eq!(a1.start.0 - a0.start.0, tr1.start.0 - tr0.start.0, "the committed transition moved with the clip"); + assert_eq!((tr1.from, tr1.duration), (tr0.from, tr0.duration)); +} + +/// Both clips of a cut transition move together: the transition travels with them in the preview +/// and is re-added at the shared delta. +#[test] +fn dragging_both_clips_of_a_cut_carries_the_transition() { + let mut h = harness(false); + let q0 = h.state().session.active_sequence().unwrap().clone(); + let (a0, b0, tr0) = (q0.video_tracks[0].items[0].clone(), q0.video_tracks[0].items[1].clone(), q0.video_tracks[0].transitions[0].clone()); + h.state_mut().session.execute("timeline.select", json!({"clips": [a0.id.0, b0.id.0]})).unwrap(); + step(&mut h); + let a_rect = rect(&h, &format!("timeline.clip.{}", a0.id.0)); + let tr_rect = rect(&h, &format!("timeline.transition.{}", tr0.id.0)); + let gap_before = tr_rect.min.x - a_rect.min.x; + let y = a_rect.center().y; + pointer(&mut h, a_rect.center(), Some(true)); + for i in 1..=6 { + pointer(&mut h, pos2(a_rect.center().x + 20.0 * i as f32, y), None); + } + let gap_mid = rect(&h, &format!("timeline.transition.{}", tr0.id.0)).min.x - rect(&h, &format!("timeline.clip.{}", a0.id.0)).min.x; + assert!((gap_mid - gap_before).abs() < 1.0, "transition lagged: gap {gap_before} -> {gap_mid}"); + pointer(&mut h, pos2(a_rect.center().x + 140.0, y), Some(false)); + let q1 = h.state().session.active_sequence().unwrap().clone(); + let (a1, b1, tr1) = (q1.video_tracks[0].items[0].clone(), q1.video_tracks[0].items[1].clone(), q1.video_tracks[0].transitions[0].clone()); + assert_eq!(a1.start.0 - a0.start.0, tr1.start.0 - tr0.start.0); + assert_eq!(b1.start.0 - b0.start.0, tr1.start.0 - tr0.start.0, "the transition moved with both clips"); +} + +/// XYZ with cut transitions on both cuts: dragging YZ leaves the X↔Y transition where it is +/// while dragging (a preview of the drop that deletes it, as in Premiere) and the Y↔Z one +/// travels with the pair. +#[test] +fn dragging_two_clips_drops_the_transition_on_the_cut_that_stayed() { + let mut h = harness_xyz(); + let q0 = h.state().session.active_sequence().unwrap().clone(); + let (x0, y0, z0) = (q0.video_tracks[0].items[0].clone(), q0.video_tracks[0].items[1].clone(), q0.video_tracks[0].items[2].clone()); + let (xy0, yz0) = (q0.video_tracks[0].transitions[0].clone(), q0.video_tracks[0].transitions[1].clone()); + h.state_mut().session.execute("timeline.select", json!({"clips": [y0.id.0, z0.id.0]})).unwrap(); + step(&mut h); + let y_rect = rect(&h, &format!("timeline.clip.{}", y0.id.0)); + let xy_rect = rect(&h, &format!("timeline.transition.{}", xy0.id.0)); + let yz_rect = rect(&h, &format!("timeline.transition.{}", yz0.id.0)); + let gap_before = yz_rect.min.x - y_rect.min.x; + let y_center = y_rect.center().y; + // press in the middle of Y, clear of both transition bands and the edge zones + let x0p = y_rect.left() + 60.0; + pointer(&mut h, pos2(x0p, y_center), Some(true)); + for i in 1..=6 { + pointer(&mut h, pos2(x0p + 20.0 * i as f32, y_center), None); + } + // mid-drag: the X↔Y transition stays on its cut (the drop will remove it), the Y↔Z one + // follows the pair + assert_eq!(rect(&h, &format!("timeline.transition.{}", xy0.id.0)), xy_rect, "the X↔Y transition stays on its cut while dragging"); + let gap_mid = rect(&h, &format!("timeline.transition.{}", yz0.id.0)).min.x - rect(&h, &format!("timeline.clip.{}", y0.id.0)).min.x; + assert!((gap_mid - gap_before).abs() < 1.0, "Y↔Z transition lagged: gap {gap_before} -> {gap_mid}"); + pointer(&mut h, pos2(x0p + 140.0, y_center), Some(false)); + let q1 = h.state().session.active_sequence().unwrap().clone(); + let (x1, y1, z1) = (q1.video_tracks[0].items[0].clone(), q1.video_tracks[0].items[1].clone(), q1.video_tracks[0].items[2].clone()); + assert_eq!(x1.start, x0.start, "X is not part of the drag"); + assert_eq!(y1.start.0 - y0.start.0, z1.start.0 - z0.start.0, "Y and Z moved together"); + assert_eq!(q1.video_tracks[0].transitions.len(), 1, "the X↔Y transition was dropped with the move"); + assert!(!q1.video_tracks[0].transitions.iter().any(|t| t.id == xy0.id), "its id is gone"); + let yz1 = &q1.video_tracks[0].transitions[0]; + assert_eq!(yz1.start.0 - yz0.start.0, y1.start.0 - y0.start.0, "the Y↔Z transition traveled with the pair"); +} + +/// Option-drag copies the clip and its fade: the original keeps its transition at home, the copy +/// shows a ghost beside it while dragging and keeps its own transition on the drop. +#[test] +fn option_drag_copies_show_a_ghost_transition_with_the_copy() { + let mut h = harness(true); + let q0 = h.state().session.active_sequence().unwrap().clone(); + let a0 = q0.video_tracks[0].items[0].clone(); + let tr0 = q0.video_tracks[0].transitions[0].clone(); + let a_rect = rect(&h, &format!("timeline.clip.{}", a0.id.0)); + let tr_rect = rect(&h, &format!("timeline.transition.{}", tr0.id.0)); + let y = a_rect.center().y; + // hold Option for the whole drag, as the real drag does + h.input_mut().events.push(egui::Event::ModifiersChanged(Modifiers { alt: true, ..Default::default() })); + let x0 = a_rect.left() + 110.0; // clear of the fade band and the edge zones + pointer(&mut h, pos2(x0, y), Some(true)); + for i in 1..=6 { + pointer(&mut h, pos2(x0 + 20.0 * i as f32, y), None); + } + // a ghost transition beside the copy; the original stays exactly where it was + let ghost = rect(&h, &format!("timeline.transition.{}.copy", tr0.id.0)); + assert_eq!(rect(&h, &format!("timeline.transition.{}", tr0.id.0)), tr_rect, "the original transition stays at home"); + assert!((ghost.min.x - tr_rect.min.x) > 60.0, "ghost {ghost:?} did not follow the copy from {tr_rect:?}"); + pointer(&mut h, pos2(x0 + 140.0, y), Some(false)); + h.input_mut().events.push(egui::Event::ModifiersChanged(Modifiers::NONE)); + step(&mut h); + let q1 = h.state().session.active_sequence().unwrap().clone(); + assert_eq!(q1.video_tracks[0].transitions.len(), 2, "the copy carried its own transition"); + let (home, copy_tr) = (q1.video_tracks[0].transitions[0].clone(), q1.video_tracks[0].transitions[1].clone()); + assert_eq!(home.start, tr0.start, "the original's transition stayed"); + assert_ne!(copy_tr.id, tr0.id, "the copy's transition is a fresh one"); + let copy = q1.video_tracks[0].items.iter().find(|i| i.id != a0.id).expect("a new clip appeared"); + assert_eq!(copy_tr.to, Some(copy.id), "the copy's fade sits on the copy"); +} + +/// Dragging one side of a cut transition to another track drops it (Premiere); the preview leaves +/// it where it is, and the drop removes it. +#[test] +fn dragging_one_side_to_another_track_drops_the_transition() { + let mut h = harness(false); + // a clip far to the right on V2 marks that track's row + let q = h.state().session.active_sequence().unwrap().clone(); + let (v2, rate) = (q.video_tracks[1].id, h.state().session.sequence_rate()); + h.state_mut().session.execute("timeline.place", json!({"item":5,"time":rate.tick_of(200).0,"duration":rate.tick_of(24).0,"track": v2.0})).unwrap(); + step(&mut h); + let q0 = h.state().session.active_sequence().unwrap().clone(); + let (a0, tr0) = (q0.video_tracks[0].items[0].clone(), q0.video_tracks[0].transitions[0].clone()); + let a_rect = rect(&h, &format!("timeline.clip.{}", a0.id.0)); + let v2_rect = rect(&h, &format!("timeline.clip.{}", q0.video_tracks[1].items[0].id.0)); + let tr_rect = rect(&h, &format!("timeline.transition.{}", tr0.id.0)); + pointer(&mut h, a_rect.center(), Some(true)); + for i in 1..=6 { + let p = pos2(a_rect.center().x + 20.0 * i as f32, a_rect.center().y + (v2_rect.center().y - a_rect.center().y) * i as f32 / 6.0); + // move into the V2 row first (track crossing), so the pointer ends up there + pointer(&mut h, p, None); + } + assert_eq!(rect(&h, &format!("timeline.transition.{}", tr0.id.0)), tr_rect, "the doomed transition stays at home while dragging"); + pointer(&mut h, pos2(a_rect.center().x + 140.0, v2_rect.center().y), Some(false)); + let q1 = h.state().session.active_sequence().unwrap().clone(); + assert!(q1.video_tracks[0].transitions.is_empty(), "the clip moved to V2 and its transition was dropped"); + assert!(q1.video_tracks.iter().any(|t| t.items.iter().any(|i| i.id == a0.id && t.id == v2)), "the clip landed on V2"); +} From 8cd44f205b1f581f3c384a3f323a4725d0d5bf02 Mon Sep 17 00:00:00 2001 From: Ikki Nishihara Date: Mon, 12 Oct 2026 12:33:52 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=E3=83=AD=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=83=88=E3=83=A9=E3=83=83=E3=82=AF=E3=81=AE?= =?UTF-8?q?=E6=96=9C=E7=B7=9A=E8=A1=A8=E7=A4=BA=E3=81=A8=E3=83=89=E3=83=A9?= =?UTF-8?q?=E3=83=83=E3=82=B0=E4=BF=9D=E8=AD=B7=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#1191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/panels/timeline.rs | 108 ++++++++++++++++++----- crates/ui-egui/tests/timeline_trim_ui.rs | 82 +++++++++++++++++ 2 files changed, 168 insertions(+), 22 deletions(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index ac4caf53..83bc4bdb 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -575,6 +575,14 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { app.auto.add(&format!("timeline.transition.{}.in", trn.id.0), Rect::from_min_max(tr_rect.min, pos2(x0 + zone, tr_rect.max.y)), "start"); app.auto.add(&format!("timeline.transition.{}.out", trn.id.0), Rect::from_min_max(pos2(x1 - zone, tr_rect.min.y), tr_rect.max), "end"); } + if tr.locked { + let color = Color32::from_rgba_unmultiplied(t.text.r(), t.text.g(), t.text.b(), 64); + let mut x = content.min.x - row.height(); + while x < content.max.x { + p.line_segment([pos2(x, row.max.y), pos2(x + row.height(), row.min.y)], Stroke::new(1.0, color)); + x += 8.0; + } + } } // ---- track headers @@ -1849,6 +1857,26 @@ fn marquee_clips(seq: &Sequence, layout: &Layout, r: Rect) -> Vec { .collect() } +/// A move previews the same track-lock restrictions as its eventual edit. +fn drag_is_locked(session: &filmcraft_engine::Session, seq: &Sequence, drag: &Drag) -> bool { + let clips = match drag { + Drag::Move { clips, track_delta, .. } => return move_is_locked(seq, clips, *track_delta), + Drag::Trim { clip, .. } | Drag::Stretch { clip, .. } | Drag::Remix { clip, .. } | Drag::Slip { clip, .. } | Drag::Slide { clip, .. } => vec![*clip], + Drag::Roll { left, right, .. } => vec![*left, *right], + Drag::Transition { id, .. } => return seq.all_tracks().any(|t| t.locked && t.transitions.iter().any(|tr| tr.id == *id)), + _ => return false, + }; + move_is_locked(seq, &filmcraft_engine::commands::with_links(session, &clips), 0) +} + +fn move_is_locked(seq: &Sequence, clips: &[ClipId], delta: i32) -> bool { + clips.iter().any(|clip| { + seq.find_item(*clip).is_some_and(|(track, _)| { + seq.track(track).is_some_and(|t| t.locked) || shift_track(seq, track, delta).and_then(|id| seq.track(id)).is_some_and(|t| t.locked) + }) + }) +} + fn shift_track(seq: &Sequence, tid: TrackId, delta: i32) -> Option { if delta == 0 { return Some(tid); @@ -2188,17 +2216,35 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & && area.contains(p) { let g = grab_at(seq, layout, p, tool, mods, roll_ripple); - let cur = match (tool, g) { - (_, Grab::Edge { .. }) => CursorIcon::ResizeColumn, - (Tool::Selection, Grab::Other(Hit::Transition { edge: Some(_), .. })) => CursorIcon::ResizeColumn, - (Tool::Selection, Grab::Other(Hit::Transition { edge: None, .. })) => CursorIcon::Grab, - (_, Grab::Volume { .. }) if tool == Tool::Pen || mods.command => CursorIcon::Crosshair, - (_, Grab::Volume { .. }) => CursorIcon::ResizeVertical, - (Tool::Razor, Grab::Other(Hit::Clip { .. })) => CursorIcon::Crosshair, - (Tool::Slip | Tool::Slide, Grab::Other(Hit::Clip { .. })) => CursorIcon::ResizeHorizontal, - (Tool::Hand, _) => CursorIcon::Grab, - (Tool::Zoom, _) => CursorIcon::ZoomIn, - _ => CursorIcon::Default, + let blocked = tool == Tool::Selection + && match g.hit() { + Hit::Clip { clip, .. } => { + let clips = if app.session.state.selection.contains(&clip) { + app.session.state.selection.clone() + } else if mods.alt { + vec![clip] + } else { + filmcraft_engine::commands::with_links(&app.session, &[clip]) + }; + move_is_locked(seq, &clips, 0) + } + _ => false, + }; + let cur = if blocked { + CursorIcon::NotAllowed + } else { + match (tool, g) { + (_, Grab::Edge { .. }) => CursorIcon::ResizeColumn, + (Tool::Selection, Grab::Other(Hit::Transition { edge: Some(_), .. })) => CursorIcon::ResizeColumn, + (Tool::Selection, Grab::Other(Hit::Transition { edge: None, .. })) => CursorIcon::Grab, + (_, Grab::Volume { .. }) if tool == Tool::Pen || mods.command => CursorIcon::Crosshair, + (_, Grab::Volume { .. }) => CursorIcon::ResizeVertical, + (Tool::Razor, Grab::Other(Hit::Clip { .. })) => CursorIcon::Crosshair, + (Tool::Slip | Tool::Slide, Grab::Other(Hit::Clip { .. })) => CursorIcon::ResizeHorizontal, + (Tool::Hand, _) => CursorIcon::Grab, + (Tool::Zoom, _) => CursorIcon::ZoomIn, + _ => CursorIcon::Default, + } }; ctx.set_cursor_icon(cur); // the edge a press here would grab, and the trim it starts (#259) @@ -2378,6 +2424,13 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & } (_, Grab::Other(Hit::Empty { .. } | Hit::None)) => None, }; + let started = started.filter(|drag| { + let blocked = drag_is_locked(&app.session, seq, drag); + if blocked { + ctx.set_cursor_icon(CursorIcon::NotAllowed); + } + !blocked + }); if let Some(d) = started { if matches!(d, Drag::Scrub { .. }) { scrub_playhead(app, seq, layout, p, mods.shift); @@ -2456,17 +2509,22 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & (Some(c), Some(s)) if c.kind == s.kind => c.index as i32 - s.index as i32, _ => 0, }; - // #1104: past the last track of the clips' kind, a new track (only for clips of one track) - let new_track = start_row.filter(|_| clips.iter().all(|c| seq.find_item(*c).is_some_and(|(t, _)| t == start_track))).and_then(|s| { - let (vh, ah) = (app.ui.timeline.video_track_h, app.ui.timeline.audio_track_h); - new_track_at(seq, layout, p.y, vh, ah).filter(|(kind, _)| *kind == s.kind) - }); - if let Some((_, band)) = new_track { - let (x0, x1) = (layout.x_of((first + offset).max(Tick::ZERO)), layout.x_of((last + offset).max(Tick::ZERO))); - let r = Rect::from_min_max(pos2(x0, band.min.y + 1.0), pos2(x1, band.max.y - 1.0)); - ui.painter().rect_stroke(r, 3.0, Stroke::new(1.5, Color32::WHITE), StrokeKind::Inside); + if move_is_locked(seq, &clips, track_delta) { + ctx.set_cursor_icon(CursorIcon::NotAllowed); + Some(Drag::Move { clips, grab_tick, start_track, offset: Tick::ZERO, track_delta: 0, new_track: None, copy: mods.alt }) + } else { + // #1104: past the last track of the clips' kind, a new track (only for clips of one track) + let new_track = start_row.filter(|_| clips.iter().all(|c| seq.find_item(*c).is_some_and(|(t, _)| t == start_track))).and_then(|s| { + let (vh, ah) = (app.ui.timeline.video_track_h, app.ui.timeline.audio_track_h); + new_track_at(seq, layout, p.y, vh, ah).filter(|(kind, _)| *kind == s.kind) + }); + if let Some((_, band)) = new_track { + let (x0, x1) = (layout.x_of((first + offset).max(Tick::ZERO)), layout.x_of((last + offset).max(Tick::ZERO))); + let r = Rect::from_min_max(pos2(x0, band.min.y + 1.0), pos2(x1, band.max.y - 1.0)); + ui.painter().rect_stroke(r, 3.0, Stroke::new(1.5, Color32::WHITE), StrokeKind::Inside); + } + Some(Drag::Move { clips, grab_tick, start_track, offset, track_delta, new_track, copy: mods.alt }) } - Some(Drag::Move { clips, grab_tick, start_track, offset, track_delta, new_track, copy: mods.alt }) } Drag::Trim { clip, edge, mode, from, .. } => seq.find_item(clip).map(|(_, it)| { let base = if edge == filmcraft_edit::Edge::In { it.start } else { it.end() }; @@ -2523,7 +2581,13 @@ fn interact(app: &mut FilmcraftApp, ui: &mut egui::Ui, seq: &Sequence, layout: & } other => Some(other), }; - app.tl.drag = new; + app.tl.drag = new.filter(|drag| { + let blocked = drag_is_locked(&app.session, seq, drag); + if blocked { + ctx.set_cursor_icon(CursorIcon::NotAllowed); + } + !blocked + }); // auto-scroll when dragging near the edges if !matches!(app.tl.drag, Some(Drag::Pan { .. }) | Some(Drag::ZoomBar { .. }) | Some(Drag::Volume(_))) { let v = &mut app.ui.timeline; diff --git a/crates/ui-egui/tests/timeline_trim_ui.rs b/crates/ui-egui/tests/timeline_trim_ui.rs index c68d5612..816bc5c0 100644 --- a/crates/ui-egui/tests/timeline_trim_ui.rs +++ b/crates/ui-egui/tests/timeline_trim_ui.rs @@ -390,3 +390,85 @@ fn dragging_in_the_trim_monitor_trims_without_hanging() { assert!(e1 < e0, "the Out edge moved in by {} frames", frames.abs()); assert!((6..=14).contains(&-frames), "about 11 frames expected for 60 px at 6 px/frame, moved {}", frames.abs()); } + +fn locked_move_keeps_the_original_preview(audio: bool, destination: bool, linked_partner: bool, edge: Option<&str>) { + let mut d = Driver::demo(); + d.exec("sequence.linkedSelection", json!({"on": linked_partner})); + let seq = d.app().session.active_sequence().unwrap(); + let tracks = if audio { &seq.audio_tracks } else { &seq.video_tracks }; + let clip = tracks[0].items[0].id; + let locked = if linked_partner { seq.audio_tracks[0].id } else { tracks[usize::from(destination)].id }; + d.exec("timeline.setTrack", json!({"track": locked.0, "locked": true})); + d.frames(3); + let prefix = format!("timeline.clip.{}", clip.0); + let original = d.ok("ui.elements", json!({"prefix": prefix}))[0]["rect"].clone(); + let r = original.as_array().unwrap(); + let from = pos2(r[0].as_f64().unwrap() as f32 + r[2].as_f64().unwrap() as f32 * 0.5, r[1].as_f64().unwrap() as f32 + 8.0); + let from = if let Some(edge) = edge { + let point = d.ok("ui.timeline.locate", json!({"clip": clip.0, "edge": edge})); + pos2(point["x"].as_f64().unwrap() as f32, from.y) + } else { + from + }; + let mut to = from + egui::vec2(60.0, 0.0); + if destination { + let label = if audio { "A2" } else { "V2" }; + let rows = d.ok("ui.elements", json!({"prefix": format!("timeline.track.{label}.target")})); + let y = &rows[0]["rect"]; + to.y = (y[1].as_f64().unwrap() + y[3].as_f64().unwrap() * 0.5) as f32; + } + let before = d.app().session.project.clone(); + let undo = d.app().session.history.undo.len(); + d.harness.input_mut().events.push(egui::Event::PointerMoved(from)); + d.frames(1); + d.harness.input_mut().events.push(egui::Event::PointerButton { + pos: from, + button: egui::PointerButton::Primary, + pressed: true, + modifiers: Default::default(), + }); + d.frames(1); + for i in 1..=8 { + d.harness.input_mut().events.push(egui::Event::PointerMoved(from + (to - from) * (i as f32 / 8.0))); + d.frames(1); + } + d.frames(2); + let elements = d.ok("ui.elements", json!({"prefix": prefix})); + assert_eq!(elements[0]["rect"], original, "a prohibited move never previews a displaced clip"); + assert_eq!(d.cursor(), CursorIcon::NotAllowed); + d.harness.input_mut().events.push(egui::Event::PointerButton { + pos: to, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: Default::default(), + }); + d.frames(2); + assert_eq!(serde_json::to_value(&*d.app().session.project).unwrap(), serde_json::to_value(&*before).unwrap()); + assert_eq!(d.app().session.history.undo.len(), undo); +} + +#[test] +fn locked_sources_do_not_preview_a_move() { + for audio in [false, true] { + locked_move_keeps_the_original_preview(audio, false, false, None); + } +} +#[test] +fn locked_destinations_do_not_preview_a_move() { + for audio in [false, true] { + locked_move_keeps_the_original_preview(audio, true, false, None); + } +} +#[test] +fn a_locked_linked_partner_prevents_the_move_preview() { + locked_move_keeps_the_original_preview(false, false, true, None); +} + +#[test] +fn locked_edges_do_not_preview_a_trim() { + for audio in [false, true] { + for edge in ["in", "out"] { + locked_move_keeps_the_original_preview(audio, false, false, Some(edge)); + } + } +} From 69a6c4b6258426c94b283b5772904214955c1909 Mon Sep 17 00:00:00 2001 From: lloyd <34131158+LloydNicholson@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:09:57 +0200 Subject: [PATCH 05/18] packaging: Linux file managers offer FilmCraft for .fcproj projects and video files The desktop entry declared no MimeType and the shared-mime-info file was an empty stub, so no file manager listed FilmCraft under "Open With" for any file, and a double-clicked .fcproj had no type to open with. The app already handles both from the command line (`%F`): a project opens, media is imported (#850). - mime.xml: define application/x-filmcraft for *.fcproj, a JSON subclass with the app icon; the same name the macOS UTI declares in Info.plist.in. - Desktop entry: MimeType lists the project type and the video containers FilmCraft imports (MP4/M4V, QuickTime, Matroska, WebM, AVI, MXF, MPEG-TS, MPEG-PS, 3GP). Audio and stills are left out so an NLE doesn't crowd every image and song's Open With list. - AppStream: the metainfo the project type. - Flatpak bundle: the comment no longer says the MIME file has no types. Co-Authored-By: Claude Opus 5.5 --- .../__pycache__/test_install.cpython-313.pyc | Bin 0 -> 11481 bytes .../linux/ai.storyteller.filmcraft.desktop | 1 + .../ai.storyteller.filmcraft.metainfo.xml.in | 1 + .../linux/ai.storyteller.filmcraft.mime.xml | 9 +++++++++ .../flatpak/ai.storyteller.filmcraft.bundle.yml | 4 ++-- 5 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 packaging/linux/__pycache__/test_install.cpython-313.pyc diff --git a/packaging/linux/__pycache__/test_install.cpython-313.pyc b/packaging/linux/__pycache__/test_install.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b771ec8f391c4513cc04cf9f9ad2c17eb19c6f0 GIT binary patch literal 11481 zcmcgydu$uWnO~Aia!D;EQq+qQWizrYi*hL1mi(6dlx!)nWSc7{PN;Z8kt>N2MY6L? z$7Y)}ZLfE-ae%D5rl#8fR)YeO(cr2g0qOz;n%)J7fy*7Zp%Myf9dJeOA0KdlAH6Fs z|J;4E%Oxdh?KC~$M$qhVcAnqNe81oK%|5o-tOSIG-+cPo=S>9hd;CKU#$w{}zeD0} zf*}yW=otO;I;4ZA;d%XY24wIkc_bp`cSdBCpC)9IpJrs1pA@3xrv+KY4Q<4@b+1lo zJ|p~bfzgB>*nFr6@kNzc2CCQ zvk?@Y6e8=#SXzYZkoz_nkg15Vs{zr{B*xwL}jqQW@iTfxizzCL+a^ zm76m`jMw7vE{NYILUJaK0>$o18IXWAh5dC<7bN1Pw?QIcUSCE@LoT^FH$ZL9Qzz&l zLRIS9C)B3Z_vNQr8%9(b1=OxCmrzrT!3XU_xzZS*oW^1>BrX?ls`biG#>kis>O9bf zW<7JUefhlNT2M}7Q5ov1+7C6TeQKSrHX_ulzGo~-pPJ*5->I7hr28kg8gi@cR}+l2 zM_=sYiF45^U+$NwauP#usuQqlG{-5+JE)VTvQ_Q5{A6f#jXlIj2SHR3pwVp5WPDJCJWB^?>c3UTzQ(?p<1YR=o(~y$ra4Hp#MP#kSe`zhY z)y+?TNp(j#env>9ZtEpWske-+l=Ma7vCPqJ&-kHfKR<0vp;$tg^z7Kqk0*9`+S+%u zL3FIma|9lppzspmSx(OO`s3krA~MZI+pJzg#uZIQ{H0d?XNFG>jxvM3k>RmSZ7h;Z z@cwCd#FNM`fHKK6%*JLpe<~cA2~TmnoY#GMHty9Ue2^uIyBy;MHaU}_KoUY4aaONG zq6BU>1$}T*?HHtzD2%QQ6m{?wiNtRuYd9J`8Rx=@bV@Qulmx;`N+R*lB@!i*f@CTR z)Hpi>b3#}-NG5(dEyUsy6-lP9$i0w5gMmTGbo|^oc6dOtzK&u7#|p42utf3#cu9%m zx%ec)SSFDv(EO4K5M_!Jc&x%a5Tv+&Itg<{X|1q{R%oHyCg- zqn!WsB$^Qr2ZD*m66wo6EG(aJB?TFZ$YO~p<&`WsR$cJa71$A+5KKw6>os?`oQJg% ze-`xjTf|zOyHMxy%42G~b9V#By!DVnZ)vRIOTU^DS3fuBSzdyFhIdsclOGIsDwcTJO3M zS_m!f7wfyAq!pKJzS?{}AzIoC)J~DwxeO)Yd2o%Yp0Bx9^A4H)me{m&Y3%2r+o3ys zqVMzyHB@}JM%#ZddHq86LeAMC(j80W&#kwu%NNDYlb<%+rE=#kclGvol}NU60JT>SK`WMqI=wff42L z4)=8=s@#{ktwLqwZltG=UH_u~jSwa%EBtb9+_#+wPL1R1N zHH}K-M3|pO81BaIW10oK*${a>Dpg0qDSYJEWLik21$?kz%bNw*gplJkNmf2RkwVD` z$Mcc_r4y2Y7ZAe8A{l1GDdd9GZ9Qtk86=zrp2vvq@u2NE$6Va4Qr(6ZM1JrlBvYz5;A;Dla_&jdv5g4kx8xkb6Z>uN( zKZB$u)3HFl2D4E)H;mj0`1u_ew-MUaU^<}^)(1+gq1<~L0dlR9S%DG9>q=|b1L}_6 zFkh{?Y-lskQtnSpsaC)fWW8=gPphqt2iU~-6dZqoY<0!eYgPL%KT9Jqlv~r5BuI*W z60=cT*&rE>)qpl^U#kr*>FNVdnFs9ZBA@rbjvk2@<)UM%<@NQbGY39cqwv9m3Ff;^ z%`ZP0dr$|Z96g4h>1jQd`wO+JwFAKJDg#&?wW@ruF2EuD4D31e2NCE}a@WVWR>-I3 z1WjrSem$ISCuVt5@aL@?&DhdBn2VqYr%-s7&v;}m=3h_6{7En|kxIeF5LU_=YdV3r zco=A%)vK3`lQG1Dc1@?EI4iSNY2}G*>PyP!Q}`jmVn_I_A`AzaCZ(nZ*>Wpyv?Yl|Iare3!!U7#=^cS0hlu$c(p9Mgauk zCJ;W4$b%z{5ojMoQdO7-=?R11NQdK+6>$JKiAXZa0SyzP96~_kxIDxQLA{k!?3ObE;;W6yS(H@Ar zW*Mgt*1(ccUN6afi9u{6d^#>D^v+zIF^{1gGdlmff`x zYgy`8-g~?IPOsQ?^yAu38h_iE_YAMtpMx?5yGOKpR_j}`+``4hDA2<~-9E8yU%u|Z zo!;LZ{?*~T_2S{t-1(q*I9NCo77vBx=3czGLbEW?`LkEgW?#s491~lQ72Lz3dpPfYeuX}>?$v+cPNE4$he|bGBWzWlh;j z+38%zp>ik-jW4`ZNU#`htsM;k~?OLN7=0~rM=G;9WGItt3 z7|*qwxZ9qm&pt2^^lq88NQS9cGSdx_C&qgc92W%}P)K^X^`5KZcJvg@Fr-a&J_RlD zl~jiaU|LiW82vgDfsJJi!UNPui>?|VUxU(=Nd&Z3(Hs7$xKCu$TZ7c7kO!nk$0+2X zT0{oayP2UvC8!LNs)ApaS0Y$`lCdyWrs|+U?p224gfUdF4xX{8 zwSg32GG_`JpXT&bktci@2$(T|iE>3yXZbW5vTmeBm{muy?oD|Pdf7z^`@ln-Z7w@p zv|Otoo%PO&nG(s-TKoUE@s|N#ahvq0n5+0|+rE)bg8pptv`3Tico=zJ6=IQ@S3To= z91I*!hi5j4I0O<0*D6FA$LtjkMzgY07WLtm81&FNh$NG8rpE4Lzy_ZLxQhsc^=*d6 zZW7h;aX*T)Y&ffgj#J;P`@7S>%lySruU$bvjCSYhz!;;eE-mvhKa6GX)#6`-ngZfy(6p z6N+*LV_E(y{~m#Tj-Aj`#52URgo*}HSd&RGEVjZMFfHLitKYDdnACZz;X+APT%d!n z{*ox!a)@2x)C`^9tIJq?zI0+Hk$gQ-%+476e!rTg{K9O?-`(w3nlOPD!G;Dk5svob zQ!b~rmr_=of^IPo$@&ts3?|A7cgKh&nZf@v2CYcd3c(&tDm-3h_KKS8g>oCbK3zqI zD_aU>CBjaIV{vwZn*5L@HrvlEMhTDNL0*fqhIhbWJ zY^di@bIZ@5PRlK&&QsM3_zZ#*G~?KS-uN~|ssa>)eJJ0itfLL?;&qICZ5&C?rsAAX zx75}FxodBGkSqJ?bj7&KbLpXH|BGvdk6^?^>_#)(Z4P`4-Msgl6Y01FcQ9L|q zudH%?!A6sS)QM!t_LO?HPR>`=nt-l&9wIn4iYq_Y*diJm z+!6aiYml)YuG7MnQ zl|i8eU?$-i$w5m2fT>{#m5zPcXk#`2K2ycejLmP{MTG37)@s#M`8lX7w*@MxIG1&K z<&Pn%yY++cg$w-EF!qB+*&A73Zb?mn4^qQ_%{U-m!&42prJHrN1%kkC#In&FDtS@C z^uyf0B9y(A>i838*ytK#3*jv$hhS+{7-xBVhw#pogZ4KuK{MmP=S=NKPb8?&j$##p zscR;JQ~-8OWl)u%g4&p`(TA&N0o@u6KYcdEW(Nqw5YuXuuWDG z)G@BxQ4O*O9J~A~f?Bpbj!MGzG@>@8(zM1=)DA66W?*MrnCBEnuk4!5Y%5=_bCKz! zXU8e9JWt{;fxrfpZ?<<>WB&otkVyFENI=*lodFIeUyQ>Ieunn>_?cMBH<6CTqnPRe zRhkXM-M;}Yn!$l=KY2WG`YbyJ@!%LcFdUG`fnjD0!4Z{j^ti+&goTK@afAh?Siw|R zH#E>amAQ`(%O*~_$4lT!xrZO(=E~qAlD6IAf2r8(%M7(VT zq6>4C$*^v}0m!aCQ{|5--y6ZaM}fZLa|WLRB%(b2CJW}O@7^WpW;gIkOWzci;4Y`o z5h<`)Cl?olEq>(%KH5keF4%b%JZX`%fWPlj{kGtz?~5lR0Le$XOKpA|M!@64bRRT& z&dq+-*|)g+6Vq=^pVa)eCU@?Y!m*3D&fh!_+5gRBZTH=6dvv!gdvvk?0inuQt6rDF z1xh|}>>!^_Ml0-ogfC#x35X;!tXK>s6J@7?>=TEpSA{%~ao8skITcS%NQO9ch$zW``mVCAcS<&HnMWpD*%XA4K7)Cw0Z|TiD-@H4R zqXVn77p_R_QyqnE8v2(*^ulGy0F5bMj#EGpv%fS`;aQVfH zh1OkS>#k*2zV&dS6?@e4ttUPnyL(0)yigd3iUZO7z~l-&^+z*Ny`y03_`=rlC2xaO z`1rsH^0d+LiP3u6Y4(z3BU1k624QR)#b5TYEdDx&WhE=i!fs8&xnw1pW%2b;@l6d2 zS37W5hW%iPB+H@>sH~WrqmYuQbRs6;%?AE5P?*6)9KKJGZ+_4)E_o40lQ@dwD2by5 zh@^U!g>OH>XUK*H0mUZL&{L{osyXasvm(VD+&Tz=<(PD!n{wJ;4Yl{K8~INWSKgI~((26GUJ8UvA?1k_R{`L{Ta#66=Q;euFsD+ihC zQx@h3dKdEGa1ne0q6Y?@PWL+^^n1ecFGSNl!gh~v+#{;*5jFRSDu}_~MbUFZ-+QxQ zZWYa~dGpqSxkEH}yhW~Bw!LNggDI)gP3x{7C^*|iXZs%r`R9YEfgtI*Oy1yHCF#E! Q`R>T~@%0GVERW}Z0rr_hZU6uP literal 0 HcmV?d00001 diff --git a/packaging/linux/ai.storyteller.filmcraft.desktop b/packaging/linux/ai.storyteller.filmcraft.desktop index e20ef6f9..3d9d8515 100644 --- a/packaging/linux/ai.storyteller.filmcraft.desktop +++ b/packaging/linux/ai.storyteller.filmcraft.desktop @@ -10,4 +10,5 @@ Terminal=false StartupNotify=true StartupWMClass=ai.storyteller.filmcraft Categories=AudioVideo;Video;AudioVideoEditing; +MimeType=application/x-filmcraft;video/mp4;video/x-m4v;video/quicktime;video/x-matroska;video/webm;video/vnd.avi;video/x-msvideo;application/mxf;video/mp2t;video/mpeg;video/3gpp; Keywords=video;editor;film;timeline;color;grading;nle; diff --git a/packaging/linux/ai.storyteller.filmcraft.metainfo.xml.in b/packaging/linux/ai.storyteller.filmcraft.metainfo.xml.in index 971126f3..9fd005ca 100644 --- a/packaging/linux/ai.storyteller.filmcraft.metainfo.xml.in +++ b/packaging/linux/ai.storyteller.filmcraft.metainfo.xml.in @@ -54,6 +54,7 @@ filmcraft + application/x-filmcraft diff --git a/packaging/linux/ai.storyteller.filmcraft.mime.xml b/packaging/linux/ai.storyteller.filmcraft.mime.xml index be4dcc22..6fe0feb6 100644 --- a/packaging/linux/ai.storyteller.filmcraft.mime.xml +++ b/packaging/linux/ai.storyteller.filmcraft.mime.xml @@ -1,3 +1,12 @@ + + + FilmCraft project + + + + diff --git a/packaging/linux/flatpak/ai.storyteller.filmcraft.bundle.yml b/packaging/linux/flatpak/ai.storyteller.filmcraft.bundle.yml index 4a95d0f7..db9006bf 100644 --- a/packaging/linux/flatpak/ai.storyteller.filmcraft.bundle.yml +++ b/packaging/linux/flatpak/ai.storyteller.filmcraft.bundle.yml @@ -48,8 +48,8 @@ modules: - install -Dm755 bin/filmcraft /app/bin/filmcraft - install -Dm755 bin/filmcraft-cli /app/bin/filmcraft-cli - install -Dm644 share/applications/ai.storyteller.filmcraft.desktop /app/share/applications/ai.storyteller.filmcraft.desktop - # The same MIME file the deb/rpm install (no types of its own yet; the runtime's - # shared-mime-info already knows the video and audio formats). + # The same MIME file the deb/rpm install: it defines the .fcproj project type (the + # runtime's shared-mime-info already knows the video and audio formats). - install -Dm644 share/mime/packages/ai.storyteller.filmcraft.xml /app/share/mime/packages/ai.storyteller.filmcraft.xml # Already has the release's version and date substituted by package.sh. - install -Dm644 share/metainfo/ai.storyteller.filmcraft.metainfo.xml /app/share/metainfo/ai.storyteller.filmcraft.metainfo.xml From b6866db1b2c144ef0a2f362853448b52b270aacb Mon Sep 17 00:00:00 2001 From: lloyd <34131158+LloydNicholson@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:12:21 +0200 Subject: [PATCH 06/18] Import YUV4MPEG2 (.y4m) video; stop offering .ale for import (#1292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were offered for import but could never be imported. `.y4m` is in VIDEO_EXTENSIONS (and file-format-parity.md lists Y4M as read), but nothing read it. `codecs::y4m` is a YUV4MPEG2 source written from the format's public documentation (yuv4mpeg(5)): the W/H/F/A/I/C/X stream header, FRAME lines with their own parameters, 8-bit 4:2:0 / 4:2:2 / 4:4:4 / 4:4:4 + alpha / mono and 9–16-bit little-endian (`420p10`, `422p12`, `mono16`, …) as Yuv8 / Yuv16 frames, XCOLORRANGE=FULL and the pixel aspect. Frames are indexed once from their header lines and read on demand (a reader opener too), so large files aren't loaded whole. Sizes, rates and offsets are checked; 4:1:1 and unknown colour spaces are an "unsupported" error. The Media Browser listed `.ale` among project files, but FilmCraft only writes ALE (File ▸ Export; Premiere doesn't import it either, per file-format-parity.md), so importing one always failed. It is no longer listed. Tests: hand-built Y4M files (4:2:0 frame by frame, 10-bit 4:2:2 with frame parameters, aspect and full range, mono, the reader path), truncated / bit-flipped / hostile headers under catch_unwind, and an ffmpeg oracle (8-bit 4:2:0 and 10-bit 4:2:2 files equal ffmpeg's rawvideo output byte for byte); the browser no longer classifies `.ale`. Co-Authored-By: Claude Opus 5.5 --- crates/codecs/src/lib.rs | 8 +- crates/codecs/src/y4m.rs | 299 +++++++++++++++++++++++ crates/codecs/tests/y4m_file.rs | 163 ++++++++++++ crates/engine/src/media_browser.rs | 4 +- crates/engine/src/media_browser_tests.rs | 9 + 5 files changed, 480 insertions(+), 3 deletions(-) create mode 100644 crates/codecs/src/y4m.rs create mode 100644 crates/codecs/tests/y4m_file.rs diff --git a/crates/codecs/src/lib.rs b/crates/codecs/src/lib.rs index a6a93733..9a8bebdb 100644 --- a/crates/codecs/src/lib.rs +++ b/crates/codecs/src/lib.rs @@ -16,6 +16,7 @@ //! - [`AviSource`]: AVI (OpenDML included): Motion JPEG, H.264, HEVC and uncompressed video; PCM, //! MP3 / MP2 and AC-3 audio. //! - [`AudioFileSource`]: standalone compressed audio files (MP3, FLAC, AIFF, …). +//! - [`Y4mSource`]: uncompressed YUV4MPEG2 (`.y4m`) video, frames read on demand. //! - [`openers`]: the openers to register with the engine's media pool. #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))] @@ -34,6 +35,7 @@ pub mod ogg; mod stream_color; pub mod video; mod webm_alpha; +pub mod y4m; use std::sync::{Arc, RwLock}; @@ -47,6 +49,7 @@ pub use mpeg::MpegSource; pub use mxf::MxfSource; pub use ogg::OggSource; pub use video::{DecodedFrame, VideoDecoder, VideoDecoderFactory}; +pub use y4m::Y4mSource; #[derive(Debug, thiserror::Error)] pub enum CodecError { @@ -128,9 +131,9 @@ pub fn software_video_decoder(entry: &filmcraft_isobmff::SampleEntry) -> Result< } /// Openers for the engine's media pool (MP4/MOV, Matroska/WebM, MXF, Ogg Opus/Vorbis, MPEG TS/PS and -/// MPEG-1/2 video elementary streams, APV raw bitstreams, standalone audio). +/// MPEG-1/2 video elementary streams, APV raw bitstreams, YUV4MPEG2, standalone audio). pub fn openers() -> Vec { - vec![mp4::opener, mkv::opener, mxf::opener, ogg::opener, mpeg::opener, apv::opener, avi::opener, audio::opener] + vec![mp4::opener, mkv::opener, mxf::opener, ogg::opener, mpeg::opener, apv::opener, avi::opener, y4m::opener, audio::opener] } fn reader_registry() -> &'static RwLock> { @@ -143,6 +146,7 @@ fn reader_registry() -> &'static RwLock> { ogg::reader_opener, mpeg::reader_opener, apv::reader_opener, + y4m::reader_opener, avi::reader_opener, ]) }) diff --git a/crates/codecs/src/y4m.rs b/crates/codecs/src/y4m.rs new file mode 100644 index 00000000..912cd5aa --- /dev/null +++ b/crates/codecs/src/y4m.rs @@ -0,0 +1,299 @@ +//! YUV4MPEG2 (`.y4m`) files: uncompressed planar video, as described by the format's public +//! documentation (the `yuv4mpeg(5)` manual page of mjpegtools, and the `C` colour space names +//! other tools added for high bit depths). +//! +//! The file starts with `YUV4MPEG2` and space-separated tagged parameters up to a newline: `W` and +//! `H` (size), `F` (frame rate, `n:d`), `A` (pixel aspect, `n:d`, `0:0` unknown), `I` (interlacing), +//! `C` (colour space; `420jpeg` when absent) and `X` (extensions; `XCOLORRANGE=FULL` is read). Each +//! frame is a `FRAME` line (with optional parameters of its own) followed by the Y, Cb and Cr planes +//! (and an alpha plane for `444alpha`); samples above 8 bits are 16-bit little-endian. +//! +//! Frames are found once at open (their header lines only) and read on demand, so a large file +//! is never loaded whole. Every number comes from the file and is checked before use. + +use std::sync::Arc; + +use filmcraft_color::{ColorInfo, Range}; +use filmcraft_frame::{AudioBuffer, Chroma, PixelData, VideoFrame}; +use filmcraft_media::{FrameRequest, MediaError, MediaInfo, MediaKind, MediaSource, SharedSource, VideoStreamInfo}; +use filmcraft_time::{FrameRate, Tick}; + +use crate::CodecError; + +const SIGNATURE: &[u8] = b"YUV4MPEG2"; +/// Longest stream header or `FRAME` line read. +const MAX_LINE: usize = 64 * 1024; +/// Largest picture (luma samples) accepted. +const MAX_PIXELS: u64 = 1 << 28; + +pub fn sniff(head: &[u8]) -> bool { + head.starts_with(SIGNATURE) && matches!(head.get(SIGNATURE.len()), Some(b' ' | b'\n')) +} + +/// The sample layout a `C` parameter names. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Layout { + /// `None` for luma only (`mono`). + chroma: Option, + bits: u32, + alpha: bool, +} + +impl Layout { + fn parse(c: &str) -> Option { + let (base, bits) = match c.split_once('p') { + Some((base, bits)) if !bits.is_empty() && bits.bytes().all(|b| b.is_ascii_digit()) => (base, bits.parse().ok()?), + _ => (c, 8), + }; + let (base, bits) = match base.strip_prefix("mono") { + Some("") => ("mono", bits), + Some(digits) if bits == 8 => ("mono", digits.parse().ok()?), + _ => (base, bits), + }; + if !(8..=16).contains(&bits) { + return None; + } + let (chroma, alpha) = match base { + "420" | "420jpeg" | "420paldv" | "420mpeg2" => (Some(Chroma::C420), false), + "422" => (Some(Chroma::C422), false), + "444" => (Some(Chroma::C444), false), + "444alpha" if bits == 8 => (Some(Chroma::C444), true), + "mono" => (None, false), + _ => return None, + }; + Some(Layout { chroma, bits, alpha }) + } + + fn label(self) -> String { + let sub = match (self.chroma, self.alpha) { + (None, _) => "Monochrome", + (Some(_), true) => "YUVA 4:4:4:4", + (Some(c), false) => return format!("YUV {} {}-bit", c.label(), self.bits), + }; + format!("{sub} {}-bit", self.bits) + } +} + +/// Sizes of one frame's planes in samples: luma, each chroma plane (0 for mono), alpha. +fn plane_samples(w: u32, h: u32, l: Layout) -> (usize, usize, usize) { + let luma = w as usize * h as usize; + let chroma = l.chroma.map_or(0, |c| { + let (sx, sy) = c.shifts(); + w.div_ceil(1 << sx) as usize * h.div_ceil(1 << sy) as usize + }); + (luma, chroma, if l.alpha { luma } else { 0 }) +} + +pub struct Y4mSource { + info: MediaInfo, + reader: filmcraft_media::SharedReader, + /// File offset of each frame's planes. + frames: Vec, + frame_bytes: usize, + layout: Layout, + rate: FrameRate, +} + +fn parse_ratio(s: &str) -> Option<(i64, i64)> { + let (n, d) = s.split_once(':')?; + Some((n.parse().ok()?, d.parse().ok()?)) +} + +/// The line starting at `pos` (without its newline) and the offset after it. +fn line_at(reader: &dyn filmcraft_media::reader::ByteReader, pos: u64) -> Option<(Vec, u64)> { + let mut want = 128usize; + loop { + let len = (reader.len().saturating_sub(pos)).min(want as u64) as usize; + let buf = filmcraft_media::reader::read_range(reader, pos, len).ok()?; + if let Some(end) = buf.iter().position(|&b| b == b'\n') { + let mut line = buf; + line.truncate(end); + return Some((line, pos + end as u64 + 1)); + } + if len < want || want >= MAX_LINE { + return None; + } + want = (want * 8).min(MAX_LINE); + } +} + +impl Y4mSource { + pub fn open(name: &str, bytes: Arc<[u8]>) -> crate::Result { + Self::open_reader(name, Arc::new(filmcraft_media::reader::MemReader(bytes))) + } + + pub fn open_reader(name: &str, reader: filmcraft_media::SharedReader) -> crate::Result { + let bad = |what: &str| CodecError::Container(format!("{name}: Y4M: {what}")); + let (header, mut pos) = line_at(&*reader, 0).ok_or_else(|| bad("no stream header"))?; + let header = String::from_utf8_lossy(&header).into_owned(); + let mut params = header.split(' '); + if params.next() != Some("YUV4MPEG2") { + return Err(bad("not a YUV4MPEG2 file")); + } + let (mut w, mut h, mut rate, mut par, mut colour, mut full) = (0u32, 0u32, None, (1u32, 1u32), "420jpeg".to_string(), false); + for p in params.filter(|p| !p.is_empty()) { + let (tag, v) = p.split_at(1); + match tag { + "W" => w = v.parse().map_err(|_| bad("invalid width"))?, + "H" => h = v.parse().map_err(|_| bad("invalid height"))?, + "F" => match parse_ratio(v) { + Some((n, d)) if n > 0 && d > 0 => rate = Some(FrameRate::new(n, d)), + _ => return Err(bad("invalid frame rate")), + }, + "A" => { + if let Some((n, d)) = parse_ratio(v).filter(|&(n, d)| n > 0 && d > 0) + && let (Ok(n), Ok(d)) = (u32::try_from(n), u32::try_from(d)) + { + par = (n, d); + } + } + "C" => colour = v.to_string(), + "X" => full |= v.eq_ignore_ascii_case("COLORRANGE=FULL"), + _ => {} + } + } + if w == 0 || h == 0 { + return Err(bad("missing or zero size")); + } + if u64::from(w) * u64::from(h) > MAX_PIXELS { + return Err(CodecError::Unsupported(format!("{name}: Y4M frames larger than 268 million pixels"))); + } + let layout = Layout::parse(&colour).ok_or_else(|| CodecError::Unsupported(format!("{name}: Y4M colour space {colour}")))?; + // ffmpeg's default when the file names no rate + let rate = rate.unwrap_or(FrameRate::FPS_25); + let (luma, chroma, alpha) = plane_samples(w, h, layout); + let per_sample = if layout.bits > 8 { 2 } else { 1 }; + let frame_bytes = (luma + 2 * chroma + alpha) * per_sample; + + let len = reader.len(); + let mut frames = Vec::new(); + while pos < len { + let Some((line, data)) = line_at(&*reader, pos) else { break }; + if !line.starts_with(b"FRAME") { + break; + } + let Some(end) = data.checked_add(frame_bytes as u64).filter(|&e| e <= len) else { break }; + frames.push(data); + pos = end; + } + if frames.is_empty() { + return Err(bad("no complete frames")); + } + let color = ColorInfo { matrix: filmcraft_frame::default_matrix(w, h), range: if full { Range::Full } else { Range::Limited }, ..ColorInfo::REC709 }; + let duration = Tick::from_rational(frames.len() as i64 * rate.den, 1, rate.num); + let bitrate = (frame_bytes as u64 * 8).checked_mul(rate.num as u64).map(|b| b / rate.den as u64); + let info = MediaInfo { + name: name.to_string(), + kind: MediaKind::Movie, + duration, + video: Some(VideoStreamInfo { + width: w, + height: h, + frame_rate: rate, + par, + codec: "Uncompressed YUV".into(), + pixel_format: layout.label(), + color, + has_alpha: layout.alpha, + bitrate, + hdr: None, + }), + audio_streams: Vec::new(), + container: "Y4M".into(), + start_timecode: None, + file_size: Some(len), + }; + Ok(Self { info, reader, frames, frame_bytes, layout, rate }) + } + + fn frame(&self, idx: usize) -> crate::Result { + let (Some(&at), Some(v)) = (self.frames.get(idx), self.info.video.as_ref()) else { + return Err(CodecError::Container("Y4M: frame index out of range".into())); + }; + let data = filmcraft_media::reader::read_range(&*self.reader, at, self.frame_bytes).map_err(|e| CodecError::Container(e.to_string()))?; + let (luma, chroma, alpha) = plane_samples(v.width, v.height, self.layout); + // mono: neutral chroma at 4:2:0 + let mono = Layout { chroma: Some(Chroma::C420), ..self.layout }; + let grey = plane_samples(v.width, v.height, mono).1; + let pixels = if self.layout.bits == 8 { + let plane = |i: usize, n: usize| Arc::new(data.get(i..i + n).map(<[u8]>::to_vec).unwrap_or_else(|| vec![0; n])); + let (y, cb, cr) = match self.layout.chroma { + Some(_) => (plane(0, luma), plane(luma, chroma), plane(luma + chroma, chroma)), + None => (plane(0, luma), Arc::new(vec![128; grey]), Arc::new(vec![128; grey])), + }; + let a = self.layout.alpha.then(|| plane(luma + 2 * chroma, alpha)); + PixelData::Yuv8 { planes: [y, cb, cr], chroma: self.layout.chroma.unwrap_or(Chroma::C420), alpha: a } + } else { + let max = (1u32 << self.layout.bits) - 1; + let plane = |i: usize, n: usize| { + Arc::new(match data.get(i * 2..(i + n) * 2) { + Some(b) => b.as_chunks::<2>().0.iter().map(|&s| u32::from(u16::from_le_bytes(s)).min(max) as u16).collect(), + None => vec![0; n], + }) + }; + let mid = 1u16 << (self.layout.bits - 1); + let (y, cb, cr) = match self.layout.chroma { + Some(_) => (plane(0, luma), plane(luma, chroma), plane(luma + chroma, chroma)), + None => (plane(0, luma), Arc::new(vec![mid; grey]), Arc::new(vec![mid; grey])), + }; + PixelData::Yuv16 { planes: [y, cb, cr], chroma: self.layout.chroma.unwrap_or(Chroma::C420), bits: self.layout.bits, alpha: None } + }; + Ok(VideoFrame { + width: v.width, + height: v.height, + data: pixels, + color: v.color, + par: v.par, + pts: Tick::from_rational(idx as i64 * self.rate.den, 1, self.rate.num), + }) + } +} + +impl MediaSource for Y4mSource { + fn info(&self) -> &MediaInfo { + &self.info + } + + fn video_frame(&self, req: FrameRequest) -> Result, MediaError> { + let last = self.frames.len().saturating_sub(1); + let idx = usize::try_from(self.rate.frame_at(req.time.max(Tick::ZERO))).unwrap_or(last).min(last); + Ok(Arc::new(self.frame(idx)?)) + } + + fn audio(&self, _start: i64, _frames: usize, _sample_rate: u32) -> Result { + Err(MediaError::NoStream("audio")) + } +} + +pub fn opener(name: &str, bytes: Arc<[u8]>) -> Option> { + if !sniff(&bytes) { + return None; + } + Some(Y4mSource::open(name, bytes).map(|s| Arc::new(s) as SharedSource).map_err(Into::into)) +} + +pub fn reader_opener(name: &str, head: &[u8], reader: &filmcraft_media::SharedReader) -> Option> { + if !sniff(head) { + return None; + } + Some(Y4mSource::open_reader(name, reader.clone()).map(|s| Arc::new(s) as SharedSource).map_err(Into::into)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn colour_space_names() { + let l = |c: Option, bits, alpha| Some(Layout { chroma: c, bits, alpha }); + assert_eq!(Layout::parse("420jpeg"), l(Some(Chroma::C420), 8, false)); + assert_eq!(Layout::parse("420p10"), l(Some(Chroma::C420), 10, false)); + assert_eq!(Layout::parse("422p12"), l(Some(Chroma::C422), 12, false)); + assert_eq!(Layout::parse("444alpha"), l(Some(Chroma::C444), 8, true)); + assert_eq!(Layout::parse("mono"), l(None, 8, false)); + assert_eq!(Layout::parse("mono16"), l(None, 16, false)); + for bad in ["411", "420p", "420p7", "420p17", "mono10p12", "444alphap10", "", "p10"] { + assert_eq!(Layout::parse(bad), None, "{bad}"); + } + } +} diff --git a/crates/codecs/tests/y4m_file.rs b/crates/codecs/tests/y4m_file.rs new file mode 100644 index 00000000..31623494 --- /dev/null +++ b/crates/codecs/tests/y4m_file.rs @@ -0,0 +1,163 @@ +//! YUV4MPEG2 (`.y4m`) files (#1292): File ▸ Import and the Media Browser offer them, so they must +//! open. Files are built here: a stream header of tagged parameters, then `FRAME` lines each +//! followed by the planes (Y, Cb, Cr, 16-bit little-endian above 8 bits). + +use std::sync::Arc; + +use filmcraft_color::Range; +use filmcraft_frame::{Chroma, PixelData}; +use filmcraft_media::{FrameRequest, MediaKind, SharedSource}; +use filmcraft_time::{FrameRate, Tick}; + +/// A Y4M file: `header` after the signature, `frames` as raw plane bytes, each after `frame_line`. +fn y4m(header: &str, frame_line: &str, frames: &[Vec]) -> Vec { + let mut out = format!("YUV4MPEG2 {header}\n").into_bytes(); + for f in frames { + out.extend_from_slice(frame_line.as_bytes()); + out.push(b'\n'); + out.extend_from_slice(f); + } + out +} + +/// One 8-bit frame: every Y sample `y`, every Cb `cb`, every Cr `cr`. +fn planes8(luma: usize, chroma: usize, y: u8, cb: u8, cr: u8) -> Vec { + [vec![y; luma], vec![cb; chroma], vec![cr; chroma]].concat() +} + +fn open(name: &str, bytes: Vec) -> SharedSource { + filmcraft_codecs::open_bytes(name, Arc::from(bytes)).unwrap() +} + +/// The middle of frame `k` at `fps`. +fn at(k: i64, fps: i64) -> FrameRequest { + FrameRequest::full(Tick::from_rational(2 * k + 1, 1, 2 * fps)) +} + +#[test] +fn y4m_420_opens_as_a_movie_frame_by_frame() { + // 6 × 4, 4:2:0 (chroma 3 × 2), three frames at 25 fps. + let frames: Vec> = (0..3).map(|k| planes8(24, 6, 16 + 50 * k, 100 + k, 200 - k)).collect(); + let src = open("clip.y4m", y4m("W6 H4 F25:1 Ip A1:1 C420jpeg", "FRAME", &frames)); + let info = src.info(); + assert_eq!(info.kind, MediaKind::Movie); + let v = info.video.as_ref().unwrap(); + assert_eq!((v.width, v.height, v.frame_rate), (6, 4, FrameRate::new(25, 1))); + assert_eq!(info.duration, Tick::from_units(3, 25)); + for k in 0..3u8 { + let f = src.video_frame(at(i64::from(k), 25)).unwrap(); + let PixelData::Yuv8 { planes, chroma, .. } = &f.data else { panic!("{:?}", f.data) }; + assert_eq!(*chroma, Chroma::C420); + assert_eq!((planes[0].len(), planes[1].len(), planes[2].len()), (24, 6, 6)); + assert!(planes[0].iter().all(|&s| s == 16 + 50 * k) && planes[1].iter().all(|&s| s == 100 + k) && planes[2].iter().all(|&s| s == 200 - k)); + } +} + +#[test] +fn y4m_high_bit_depth_frame_parameters_aspect_and_range() { + // 4 × 2, 4:2:2 10-bit (little-endian), 30000:1001, 4:3 pixels, full range, and frame lines + // carrying their own parameters. + let mut frame = Vec::new(); + for (n, v) in [(8, 940u16), (4, 512), (4, 64)] { + for _ in 0..n { + frame.extend(v.to_le_bytes()); + } + } + let src = open("ten.y4m", y4m("W4 H2 F30000:1001 It A4:3 C422p10 XCOLORRANGE=FULL", "FRAME Ip XFOO=1", &[frame.clone(), frame])); + let v = src.info().video.clone().unwrap(); + assert_eq!((v.frame_rate, v.par, v.color.range), (FrameRate::new(30000, 1001), (4, 3), Range::Full)); + let f = src.video_frame(at(1, 30)).unwrap(); + let PixelData::Yuv16 { planes, chroma, bits, .. } = &f.data else { panic!("{:?}", f.data) }; + assert_eq!((*chroma, *bits), (Chroma::C422, 10)); + assert!(planes[0].iter().all(|&s| s == 940) && planes[1].iter().all(|&s| s == 512) && planes[2].iter().all(|&s| s == 64)); +} + +#[test] +fn y4m_mono_has_neutral_chroma() { + let src = open("grey.y4m", y4m("W2 H2 F24:1 Cmono", "FRAME", &[vec![77; 4]])); + let f = src.video_frame(at(0, 24)).unwrap(); + let PixelData::Yuv8 { planes, .. } = &f.data else { panic!("{:?}", f.data) }; + assert!(planes[0].iter().all(|&s| s == 77) && planes[1].iter().chain(planes[2].iter()).all(|&s| s == 128)); +} + +#[test] +fn large_y4m_files_open_from_a_reader() { + let bytes: Arc<[u8]> = Arc::from(y4m("W2 H2 F25:1 C444", "FRAME", &[planes8(4, 4, 50, 60, 70), planes8(4, 4, 90, 60, 70)])); + let reader: filmcraft_media::SharedReader = Arc::new(filmcraft_media::reader::MemReader(bytes)); + let src = filmcraft_media::reader::open_reader("big.y4m", reader, &filmcraft_codecs::reader_openers(), &filmcraft_codecs::openers()).unwrap(); + let f = src.video_frame(at(1, 25)).unwrap(); + let PixelData::Yuv8 { planes, chroma, .. } = &f.data else { panic!("{:?}", f.data) }; + assert_eq!((*chroma, planes[0][0]), (Chroma::C444, 90)); +} + +#[test] +fn damaged_and_hostile_y4m_files_fail_without_panicking() { + let good = y4m("W6 H4 F25:1 C420jpeg", "FRAME", &[planes8(24, 6, 1, 2, 3), planes8(24, 6, 4, 5, 6)]); + let mut cases: Vec> = (0..good.len()).map(|n| good[..n].to_vec()).collect(); + for i in 0..good.len() { + let mut b = good.clone(); + b[i] ^= 0x41; + cases.push(b); + } + for header in + ["W99999999 H99999999 F25:1", "W0 H4 F25:1", "W6 H4 F0:0", "W6 H4 F25:0", "W6 H4 F25:1 C411", "W6 H4 F25:1 Cbogus", "W6 H4 A0:0 F-5:1", "H4 F25:1"] + { + cases.push(y4m(header, "FRAME", &[vec![0; 36]])); + } + for (i, b) in cases.into_iter().enumerate() { + let r = std::panic::catch_unwind(|| { + filmcraft_codecs::open_bytes("damaged.y4m", Arc::from(b)).map(|s| (0..3).filter(|&k| s.video_frame(at(k, 25)).is_ok()).count()) + }); + assert!(r.is_ok(), "case {i} panicked"); + } +} + +/// ffmpeg (an external oracle) writes `.y4m` files; every frame we read must equal, byte for byte, +/// the planes ffmpeg's own rawvideo output gives for the same file. +#[test] +fn ffmpeg_y4m_files_match_ffmpeg_rawvideo() { + let Some(ff) = filmcraft_testkit::oracle::ffmpeg_or_skip("Y4M oracle") else { return }; + let dir = filmcraft_testkit::fixtures::fixtures_dir("codecs/y4m"); + for (pix_fmt, bytes_per_sample) in [("yuv420p", 1usize), ("yuv422p10le", 2)] { + let y4m = dir.join(format!("testsrc2_{pix_fmt}.y4m")); + let made = filmcraft_testkit::fixtures::generate(&y4m, |tmp| { + std::process::Command::new(&ff) + .args([ + "-y", + "-v", + "error", + "-f", + "lavfi", + "-i", + "testsrc2=size=94x62:rate=25", + "-frames:v", + "5", + "-pix_fmt", + pix_fmt, + "-strict", + "-1", + "-f", + "yuv4mpegpipe", + ]) + .arg(tmp) + .status() + .is_ok_and(|s| s.success()) + }); + let Some(y4m) = made else { continue }; + let raw = + std::process::Command::new(&ff).args(["-v", "error", "-i"]).arg(&y4m).args(["-f", "rawvideo", "-pix_fmt", pix_fmt, "-"]).output().unwrap().stdout; + let src = open("oracle.y4m", std::fs::read(&y4m).unwrap()); + assert_eq!(src.info().duration, Tick::from_units(5, 25), "{pix_fmt}"); + let frame_len = raw.len() / 5; + for k in 0..5 { + let f = src.video_frame(at(k as i64, 25)).unwrap(); + let ours: Vec = match &f.data { + PixelData::Yuv8 { planes, .. } => planes.iter().flat_map(|p| p.iter().copied()).collect(), + PixelData::Yuv16 { planes, .. } => planes.iter().flat_map(|p| p.iter().flat_map(|s| s.to_le_bytes())).collect(), + other => panic!("{other:?}"), + }; + assert_eq!(ours.len(), frame_len, "{pix_fmt} frame {k}"); + assert!(ours == raw[k * frame_len..(k + 1) * frame_len], "{pix_fmt} frame {k} differs ({bytes_per_sample} bytes/sample)"); + } + } +} diff --git a/crates/engine/src/media_browser.rs b/crates/engine/src/media_browser.rs index b5a4d5da..d1e65f81 100644 --- a/crates/engine/src/media_browser.rs +++ b/crates/engine/src/media_browser.rs @@ -73,7 +73,9 @@ pub const ALL_COLUMNS: [&str; 10] = ["Name", "Media Type", "Size", "Date Modified", "Frame Rate", "Media Duration", "Video Info", "Audio Info", "Video Codec", "Audio Codec"]; pub const DEFAULT_COLUMNS: [&str; 5] = ["Name", "Frame Rate", "Media Duration", "Video Info", "Size"]; -const PROJECT_EXTENSIONS: [&str; 6] = ["fcproj", "edl", "xml", "fcpxml", "otio", "ale"]; +/// Project and interchange files the browser can import. Avid Log Exchange (`.ale`) is left out: +/// FilmCraft writes it (File ▸ Export) but, like Premiere, does not read it (#1292). +const PROJECT_EXTENSIONS: [&str; 5] = ["fcproj", "edl", "xml", "fcpxml", "otio"]; const CAPTION_EXTENSIONS: [&str; 3] = ["srt", "vtt", "scc"]; const RECENT_MAX: usize = 10; diff --git a/crates/engine/src/media_browser_tests.rs b/crates/engine/src/media_browser_tests.rs index 3c026a34..b67a1d92 100644 --- a/crates/engine/src/media_browser_tests.rs +++ b/crates/engine/src/media_browser_tests.rs @@ -318,3 +318,12 @@ fn probing_large_unstreamable_files_does_not_read_them_whole() { assert_eq!(r["items"].as_array().unwrap().len(), 1); let _ = std::fs::remove_dir_all(&dir); } + +/// #1292: an Avid Log Exchange file is something FilmCraft writes (File ▸ Export), not a project +/// it can import, so the browser doesn't list it with the project files. +#[test] +fn ale_logs_are_not_offered_for_import() { + assert_eq!(media_browser::kind_of("clips.ale"), None); + assert!(!media_browser::extensions().contains(&"ale")); + assert_eq!(media_browser::kind_of("cut.edl"), Some("project")); +} From af5d29310e326445c5a4e0a55a0027e41df37481 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Mon, 12 Oct 2026 20:56:26 +0200 Subject: [PATCH 07/18] #828: import PXR24-compressed OpenEXR Problem: OpenEXR files with PXR24 compression (a common lossy 24-bit float format from renderers and compositors) were refused with "not supported yet". Fix: decode PXR24 chunks (16 lines, deflated byte planes of per-line sample differences; floats keep their top 24 bits) into the existing scan-line pipeline. Bounds-checked, no panics on damaged data. Tests: hand-built chunks with half/float/uint channels and a partial last chunk, damaged-file cases, and an OpenImageIO-written oracle comparing our decode with OpenEXR's own (skips without oiiotool). Docs updated; stale AVI / FLAC-in-MP4 entries removed from G10. Co-Authored-By: Claude Sonnet 5.5 --- crates/media/src/still/exr.rs | 54 +++++++++++++++++++- crates/media/src/still/exr/tests.rs | 76 ++++++++++++++++++++++++++++- crates/media/tests/exr_oracle.rs | 48 ++++++++++++++++++ docs/file-format-parity.md | 4 +- docs/gaps.md | 11 +++-- 5 files changed, 182 insertions(+), 11 deletions(-) diff --git a/crates/media/src/still/exr.rs b/crates/media/src/still/exr.rs index 1def0a04..d478e3a0 100644 --- a/crates/media/src/still/exr.rs +++ b/crates/media/src/still/exr.rs @@ -1,5 +1,6 @@ //! OpenEXR stills (and the frames of EXR image sequences), read from the public OpenEXR file layout: -//! single-part scan-line files with no, RLE, ZIPS (one line per chunk) or ZIP (16 lines) compression, +//! single-part scan-line files with no, RLE, ZIPS (one line per chunk), ZIP or PXR24 (16 lines) +//! compression, //! half, float or uint channels. R, G, B and A become the frame's channels (missing colour channels //! are 0, missing alpha is opaque); a luminance-only file (Y) is shown grey. EXR values are //! scene-linear with premultiplied alpha, which is exactly the compositor's working format, so the @@ -7,7 +8,7 @@ //! //! The display window is the frame; pixels of the data window outside it are cut, and the parts of //! it the data window does not cover are transparent. Tiled, deep and multi-part files, chroma -//! subsampled channels and the PIZ, PXR24, B44 and DWA compressions are refused with a clear error. +//! subsampled channels and the PIZ, B44 and DWA compressions are refused with a clear error. mod inflate; @@ -134,12 +135,15 @@ enum Compression { None, Rle, Zip, + /// Lossy 24-bit floats: deflated byte planes of per-line differences. + Pxr24, } impl Compression { fn lines_per_chunk(self, code: u8) -> usize { match self { Compression::Zip if code == 3 => 16, + Compression::Pxr24 => 16, _ => 1, } } @@ -227,6 +231,7 @@ fn header(name: &str, b: &[u8]) -> Result
{ 0 => Compression::None, 1 => Compression::Rle, 2 | 3 => Compression::Zip, + 5 => Compression::Pxr24, _ => { let what = ["PIZ", "PXR24", "B44", "B44A", "DWAA", "DWAB"].get(usize::from(code).wrapping_sub(4)).copied().unwrap_or("unknown"); return Err(MediaError::Unsupported(format!("{name}: {what} compressed OpenEXR is not supported yet"))); @@ -329,6 +334,45 @@ fn unrle(src: &[u8], expected: usize) -> Option> { (out.len() == expected).then_some(out) } +/// Bytes of one sample in PXR24's byte planes (a float keeps its top 24 bits only). +fn plane_count(ty: PixelType) -> usize { + match ty { + PixelType::Half => 2, + PixelType::Float => 3, + PixelType::Uint => 4, + } +} + +/// Undo PXR24: per scan line and channel, the samples' differences (to the previous sample of the +/// line, modulo 2^16 / 2^24 / 2^32) are stored as byte planes, most significant first. Returns the +/// lines in the plain layout (little-endian samples). +fn unpxr24(planes: &[u8], channels: &[Channel], width: usize, lines: usize) -> Option> { + let mut out = Vec::with_capacity(planes.len().checked_mul(2)?); + let mut at = 0usize; + for _ in 0..lines { + for c in channels { + let n = plane_count(c.ty); + let len = n.checked_mul(width)?; + let plane = planes.get(at..at.checked_add(len)?)?; + at += len; + let mut acc = 0u32; + for x in 0..width { + let mut d = 0u32; + for k in 0..n { + d = d << 8 | u32::from(*plane.get(k * width + x)?); + } + acc = acc.wrapping_add(d); + match c.ty { + PixelType::Half => out.extend_from_slice(&(acc as u16).to_le_bytes()), + PixelType::Float => out.extend_from_slice(&((acc & 0xFF_FFFF) << 8).to_le_bytes()), + PixelType::Uint => out.extend_from_slice(&acc.to_le_bytes()), + } + } + } + } + (at == planes.len()).then_some(out) +} + /// Decode an EXR file into premultiplied linear RGBA floats. pub fn decode(name: &str, b: &[u8]) -> Result { let h = header(name, b)?; @@ -403,6 +447,12 @@ pub fn decode(name: &str, b: &[u8]) -> Result { Compression::None => return Err(bad(format!("chunk {i} has the wrong size"))), Compression::Rle => unpredict(unrle(data, expected).ok_or_else(|| bad(format!("chunk {i}: bad RLE data")))?).into(), Compression::Zip => unpredict(inflate::zlib(data, expected).map_err(|e| bad(format!("chunk {i}: {e}")))?).into(), + Compression::Pxr24 => { + let per_line = h.channels.iter().try_fold(0usize, |t, c| t.checked_add(plane_count(c.ty).checked_mul(dw)?)); + let planes = per_line.and_then(|l| l.checked_mul(n)).ok_or_else(|| bad("chunk size".into()))?; + let inflated = inflate::zlib(data, planes).map_err(|e| bad(format!("chunk {i}: {e}")))?; + unpxr24(&inflated, &h.channels, dw, n).ok_or_else(|| bad(format!("chunk {i}: bad PXR24 data")))?.into() + } } }; for (l, out) in rows.chunks_exact_mut(row).enumerate() { diff --git a/crates/media/src/still/exr/tests.rs b/crates/media/src/still/exr/tests.rs index 9d1e45f2..76ef15b0 100644 --- a/crates/media/src/still/exr/tests.rs +++ b/crates/media/src/still/exr/tests.rs @@ -25,6 +25,35 @@ fn predict(raw: &[u8]) -> Vec { t } +/// The test-side PXR24 packing of the `lines` of `raw` (plain layout): per line and channel, the +/// differences to the previous sample as byte planes, floats cut to their top 24 bits. +fn pxr24(raw: &[u8], chans: &[Chan], width: usize, lines: usize) -> Vec { + let mut at = 0; + let mut out = Vec::new(); + for _ in 0..lines { + for (_, t) in chans { + let size = if *t == 1 { 2 } else { 4 }; + let mut prev = 0u32; + let mut planes = vec![Vec::new(); size]; + for x in 0..width { + let b = &raw[at + x * size..at + (x + 1) * size]; + let v = match *t { + 1 => u32::from(u16::from_le_bytes([b[0], b[1]])), + 2 => u32::from_le_bytes([b[0], b[1], b[2], b[3]]) >> 8, + _ => u32::from_le_bytes([b[0], b[1], b[2], b[3]]), + }; + let d = v.wrapping_sub(prev); + prev = v; + let n = if *t == 2 { 3 } else { size }; + planes[0..n].iter_mut().enumerate().for_each(|(k, p)| p.push((d >> (8 * (n - 1 - k))) as u8)); + } + at += width * size; + out.extend(planes.concat()); + } + } + out +} + /// OpenEXR RLE: runs of 3+ equal bytes as runs, the rest as literals. fn rle(t: &[u8]) -> Vec { let mut out = Vec::new(); @@ -105,7 +134,7 @@ fn exr_file(chans: &[Chan], data: [i32; 4], display: [i32; 4], comp: u8, value: attr(&mut f, "lineOrder", "lineOrder", &[0]); attr(&mut f, "pixelAspectRatio", "float", &1f32.to_le_bytes()); f.push(0); - let lpc = if comp == 3 { 16 } else { 1 }; + let lpc = if comp == 3 || comp == 5 { 16 } else { 1 }; let ys: Vec = (data[1]..=data[3]).step_by(lpc).collect(); let table = f.len(); f.resize(table + ys.len() * 8, 0); @@ -121,6 +150,10 @@ fn exr_file(chans: &[Chan], data: [i32; 4], display: [i32; 4], comp: u8, value: let packed = match comp { 0 => raw, 1 => rle(&predict(&raw)), + 5 => { + let lines = (data[3] - y0 + 1).min(16) as usize; + zlib_stored(&pxr24(&raw, chans, (data[2] - data[0] + 1) as usize, lines)) + } _ => zlib_stored(&predict(&raw)), }; let off = f.len() as u64; @@ -178,7 +211,7 @@ fn rgba_half_every_compression() { // pixel (3, 2): R G B A let p = &px[(2 * 7 + 3) * 4..][..4]; assert_eq!(p, &[pattern(3, 3, 2), pattern(2, 3, 2), pattern(1, 3, 2), pattern(0, 3, 2)]); - for comp in [1, 2, 3] { + for comp in [1, 2, 3, 5] { let f = exr_file(RGBA_HALF, [0, 0, w - 1, h - 1], [0, 0, w - 1, h - 1], comp, pattern); assert_ne!(f, want, "compression {comp} packs differently"); let d = decode("a.exr", &f).unwrap_or_else(|e| panic!("compression {comp}: {e}")); @@ -186,6 +219,43 @@ fn rgba_half_every_compression() { } } +#[test] +fn pxr24_keeps_half_and_uint_exactly_and_floats_to_24_bits() { + // 40 wide, 35 tall: three chunks, the last of 3 lines; one channel of every type + let chans: &[Chan] = &[("B", 2), ("G", 1), ("R", 2), ("Z", 0)]; + let value = |c: usize, x: i32, y: i32| match c { + 0 => 1.0 / 3.0 + x as f32 * 0.37 - y as f32 * 1.5, // not exact in 24 bits + 1 => x as f32 * 0.0625 + y as f32, + 2 => 6000.7 - (x * y) as f32 * 1.25, + _ => (x * 1000 + y) as f32, + }; + let f = exr_file(chans, [0, 0, 39, 34], [0, 0, 39, 34], 5, value); + let d = decode("p.exr", &f).unwrap(); + assert_eq!((d.frame.width, d.frame.height), (40, 35)); + assert_eq!(d.pixel_format, "RGB 32-bit float"); + let px = floats(&d); + for (x, y) in [(0, 0), (1, 0), (39, 0), (0, 16), (7, 17), (39, 34), (13, 33)] { + let p = &px[(y * 40 + x) * 4..][..4]; + let (x, y) = (x as i32, y as i32); + let cut = |v: f32| f32::from_bits(v.to_bits() & 0xFFFF_FF00); + assert_eq!(p, &[cut(value(2, x, y)), half(f16(value(1, x, y))), cut(value(0, x, y)), 1.0], "pixel ({x}, {y})"); + } +} + +#[test] +fn damaged_pxr24_is_an_error() { + let mut f = exr_file(RGBA_HALF, [0, 0, 9, 19], [0, 0, 9, 19], 5, pattern); + assert!(decode("p.exr", &f).is_ok()); + // cut the file inside the last chunk + f.truncate(f.len() - 20); + assert!(decode("p.exr", &f).is_err()); + // a chunk whose deflate data is garbage + let mut g = exr_file(RGBA_HALF, [0, 0, 9, 19], [0, 0, 9, 19], 5, pattern); + let at = g.windows(2).rposition(|w| w == [0x78, 0x01]).unwrap(); + g[at] = 0; + assert!(decode("p.exr", &g).is_err()); +} + #[test] fn luminance_only_is_grey_and_opaque() { let f = exr_file(&[("Y", 2)], [0, 0, 3, 1], [0, 0, 3, 1], 2, |_, x, y| x as f32 * 0.25 + y as f32 * 4.0); @@ -242,6 +312,8 @@ fn unsupported_files_say_why() { assert!(matches!(decode("t.exr", &tiled), Err(MediaError::Unsupported(m)) if m.contains("tiled"))); let piz = exr_file(RGBA_HALF, [0, 0, 1, 1], [0, 0, 1, 1], 4, pattern); assert!(matches!(decode("p.exr", &piz), Err(MediaError::Unsupported(m)) if m.contains("PIZ"))); + let b44 = exr_file(RGBA_HALF, [0, 0, 1, 1], [0, 0, 1, 1], 6, pattern); + assert!(matches!(decode("b.exr", &b44), Err(MediaError::Unsupported(m)) if m.contains("B44"))); let dwab = exr_file(RGBA_HALF, [0, 0, 1, 1], [0, 0, 1, 1], 9, pattern); assert!(matches!(decode("d.exr", &dwab), Err(MediaError::Unsupported(m)) if m.contains("DWAB"))); let ry = exr_file(&[("BY", 1), ("RY", 1), ("Y", 1)], [0, 0, 1, 1], [0, 0, 1, 1], 0, pattern); diff --git a/crates/media/tests/exr_oracle.rs b/crates/media/tests/exr_oracle.rs index eebd720f..6e4730a9 100644 --- a/crates/media/tests/exr_oracle.rs +++ b/crates/media/tests/exr_oracle.rs @@ -141,3 +141,51 @@ fn exr_sequence_plays_linear_float_frames() { assert!(max_diff(&ours, &want) == 0.0, "frame {i}"); } } + +/// PXR24 (lossy 24-bit floats; ffmpeg's encoder cannot write it): OpenImageIO writes the files and +/// re-writes them uncompressed (its OpenEXR reader undoes the compression), and our decode of the +/// PXR24 file must give exactly the floats of that uncompressed copy. +#[test] +fn exr_pxr24_matches_openexr() { + let tool = |args: &[&str]| std::process::Command::new("oiiotool").args(args).status().is_ok_and(|s| s.success()); + if !tool(&["--version"]) { + eprintln!("SKIPPED: oiiotool is not installed"); + return; + } + for (name, depth, channels, size) in [("f4", "float", 4, "95x63"), ("h4", "half", 4, "40x35"), ("f3", "float", 3, "17x16"), ("h3", "half", 3, "16x17")] { + let packed = dir().join(format!("pxr24_{name}.exr")); + let plain = dir().join(format!("pxr24_{name}_plain.exr")); + let ok = |p: &Path| { + filmcraft_testkit::fixtures::generate(p, |tmp| { + let t = tmp.to_string_lossy().to_string(); + if p == packed.as_path() { + tool(&[ + "--pattern", + "fill:topleft=1,0,0,0.5:topright=0,1,0,0.5:bottomleft=0,0,1,1:bottomright=1,1,1,1", + size, + &channels.to_string(), + "--noise:type=gaussian:mean=0:stddev=0.3", + "-d", + depth, + "--compression", + "pxr24", + "-o", + &t, + ]) + } else { + tool(&[packed.to_string_lossy().as_ref(), "-d", depth, "--compression", "none", "-o", &t]) + } + }) + }; + let (Some(packed), Some(_)) = (ok(&packed), ok(&plain)) else { + eprintln!("SKIPPED: oiiotool cannot write {name}"); + continue; + }; + let plain = dir().join(format!("pxr24_{name}_plain.exr")); + let decode = |p: &Path| { + floats(&filmcraft_media::open_bytes("still.exr", std::fs::read(p).unwrap().into(), &[]).unwrap().video_frame(FrameRequest::full(Tick(0))).unwrap()) + }; + let (ours, want) = (decode(&packed), decode(&plain)); + assert_eq!(max_diff(&ours, &want), 0.0, "{name}"); + } +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 1b495659..259f1193 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: every file type Premiere Pro 26.5.2 reads or writes, against ours) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** major (new file: every file type Premiere Pro 26.5.2 reads or writes, against ours) · **Target:** Adobe Premiere Pro 2026 (26.5.2) and Media Encoder 2026 Containers, stills, audio files, project and interchange files, captions and presets. Codecs inside the containers are in [codec-parity.md](codec-parity.md). Our import list is @@ -55,7 +55,7 @@ only). "Tested" names the evidence. | PNG, JPEG, TIFF, BMP, GIF | read + write | yes | PNG / TIFF / BMP / JPEG sequences, GIF | 90% | | WebP | — | yes | no | ahead | | **PSD** (layers as sequence) | read (layered) | no | — | 0% | -| **OpenEXR** | read + write | stills and sequences: scan-line, none / RLE / ZIPS / ZIP, half / float / uint, RGB(A) or Y, kept as linear float (#828); no PIZ, PXR24, B44, DWA, tiled, multi-part or deep | sequences (half float, linear RGB / premultiplied RGBA; uncompressed, RLE, ZIPS or ZIP; #806, #874) | 70% | +| **OpenEXR** | read + write | stills and sequences: scan-line, none / RLE / ZIPS / ZIP / PXR24, half / float / uint, RGB(A) or Y, kept as linear float (#828); no PIZ, B44, DWA, tiled, multi-part or deep | sequences (half float, linear RGB / premultiplied RGBA; uncompressed, RLE, ZIPS or ZIP; #806, #874) | 70% | | **DPX** | read + write | yes: luma / RGB / RGBA, 8–16-bit, filled 10/12-bit, either byte order (#821); packed 10/12-bit, Y'CbCr 4:2:2 / 4:4:4 with or without alpha (#841); no RLE | sequences (10-bit RGB, BT.709; #779) | 90% | | Targa | read + write | yes: every image type, RLE, colour maps, 15–32-bit (#821) | sequences (24-bit, 32-bit with alpha; #779) | 95% | | HEIF / HEIC | read | no | — | 0% | diff --git a/docs/gaps.md b/docs/gaps.md index 9b2a2796..ea8df5bc 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where FilmCraft falls short of Premiere Pro -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (new file: ROADMAP's "Where we are lacking" re-measured against Premiere Pro 26.5.2 and the open issue list) · **Target:** Adobe Premiere Pro 2026 (26.5.2) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** major (new file: ROADMAP's "Where we are lacking" re-measured against Premiere Pro 26.5.2 and the open issue list) · **Target:** Adobe Premiere Pro 2026 (26.5.2) Every known shortfall, one entry each, ranked by what most stops an editor using FilmCraft for real work. This is the work list: agents should prefer it over adding checklist items. Numbers are in @@ -182,14 +182,14 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement** bit rate / mode combinations ISO/IEC 11172-3 disallows but ffmpeg writes (its mono default is 384 kbit/s). OpenEXR stills and image sequences (#828): scan-line, uncompressed / RLE / ZIPS / ZIP, half / float / uint, RGB(A) or - luminance, decoded to linear float frames and bit-exact against ffmpeg. + luminance, decoded to linear float frames and bit-exact against ffmpeg; PXR24 (lossy 24-bit + float, 16 lines per chunk) decodes too, bit-exact against OpenEXR (via OpenImageIO, #828). - **Missing:** free-format MPEG audio, MP1 in Matroska (`A_MPEG/L1`), E-AC-3 adaptive hybrid transform, enhanced coupling and the extra channels of 7.1 dependent substreams (played as the - 5.1 core), AVI (#598; listed as an extension but no - demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D + 5.1 core), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, camera RAW (R3D incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345, #383), JPEG 2000 / JPEG XS MXF, the rest of CinemaDNG (lossy / JPEG XL DNG, MXF-wrapped, opcode lists, Raw - settings; DNG frames and folders import since #1065); stills: PSD, EXR beyond scan-line RLE / ZIP (PIZ, PXR24, B44, DWA, + settings; DNG frames and folders import since #1065); stills: PSD, EXR beyond scan-line RLE / ZIP / PXR24 (PIZ, B44, DWA, tiled, multi-part, deep), HEIF / HEIC, Radiance HDR, AI / EPS, and the rarer DPX layout, run-length encoding (Targa and DPX stills and sequences import since #821, packed 10/12-bit and Y'CbCr DPX since #841, deeper than 8 bits kept at 8); transparent @@ -306,3 +306,4 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement** | Date | Change | Summary | |---|---|---| | 2026-10-10 | major | Created from ROADMAP.md's "Where we are lacking" (2026-10-05); re-ranked against Premiere 26.5.2 and the 188 open issues; added `.prproj`, 10-bit / 4:2:2 camera decode, launch crashes and missing PR CI, localization, delivery formats | +| 2026-10-12 | minor | G10: OpenEXR PXR24 import | From db6f5194feba4414190c60f6eefe0cc404bf1e66 Mon Sep 17 00:00:00 2001 From: bfenty Date: Mon, 12 Oct 2026 19:52:29 +0000 Subject: [PATCH 08/18] Add edit.rippleDeleteHead (Ripple Delete Head Frame) Remove the selected clips' heads - from each in point through the playhead - and ripple the sequence closed. Linked clips act as one group through edit::ripple_trim_group; a playhead past every member's out point deletes the group whole via edit::ripple_delete_items. Refused at or before a member's in point, and on locked members. Tests: crates/engine/src/ripple_head_tests.rs (4 tests, red on main, green with the patch, negative control by stash round-trip). Fixes #1357 --- crates/engine/src/commands.rs | 52 +++++++++++++++ crates/engine/src/lib.rs | 2 + crates/engine/src/ripple_head_tests.rs | 88 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 crates/engine/src/ripple_head_tests.rs diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index ce502fce..c9c12ed5 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -1630,6 +1630,58 @@ fn build() -> Vec { })?; Ok(Value::Null) }), + cmd!("edit.rippleDeleteHead", "Ripple Delete Head Frame", ["Edit"], None, r#"{"clip":id}?"#, has_selection, |s, p| { + // #1357: remove the head — from each selected clip's in point through the playhead — + // and ripple the sequence closed. Linked clips ripple as one group (the engine's + // group trim moves every member's in point by one delta and shifts the synced + // tracks once). The playhead past every member's out point deletes the group whole. + let ph = s.playhead(); + let clips = with_links(s, &clips_p(s, p)); + // groups keyed by link id (unlinked clips: their own id) + let mut groups: Vec<(u64, Vec)> = Vec::new(); + { + let q = s.active_sequence().ok_or(EngineError::NoSequence)?; + for c in &clips { + let (_, item) = q.find_item(*c).ok_or_else(|| EngineError::Other("clip not in the active sequence".into()))?; + if (ph - item.start) <= Tick::ZERO { + return Err(EngineError::Other("the playhead is at or before the clip's in point".into())); + } + let key = item.link.unwrap_or(c.0); + match groups.iter_mut().find(|(k, _)| *k == key) { + Some((_, members)) => members.push(*c), + None => groups.push((key, vec![*c])), + } + } + } + s.edit_sequence("Ripple Delete Head Frame", |q, ctx, st| { + for (_, members) in &groups { + let live: Vec<(ClipId, Tick, Tick)> = members.iter().filter_map(|c| q.find_item(*c).map(|(_, i)| (*c, i.start, i.duration))).collect(); + if live.is_empty() { + continue; + } + let head = live.iter().map(|(_, s, _)| (ph - *s).max(Tick::ZERO)).max().unwrap_or(Tick::ZERO); + let all_whole = live.iter().all(|(_, _, d)| head >= *d); + let ids: Vec = live.iter().map(|(c, _, _)| *c).collect(); + if all_whole { + let spans = edit::ripple_delete_items(q, &ids)?; + if st.ripple_sequence_markers { + for r in spans.iter().rev() { + crate::sequence_tools::ripple_markers(&mut q.markers, r.end(), -r.duration); + } + } + continue; + } + let before = live[0].1; + edit::ripple_trim_group(q, &ids, Edge::In, head, ctx)?; + if st.ripple_sequence_markers { + let after = q.find_item(live[0].0).map(|(_, i)| i.start).unwrap_or(before); + crate::sequence_tools::ripple_markers(&mut q.markers, after, -(after - before)); + } + } + Ok(()) + })?; + Ok(Value::Null) + }), cmd!("edit.selectAll", "Select All", ["Edit"], Some("Cmd+A"), "{}", has_seq, |s, _| { let all: Vec = s.active_sequence().map(|q| q.all_tracks().flat_map(|t| t.items.iter().map(|i| i.id)).collect()).unwrap_or_default(); s.state.selection = all; diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4098893a..778312b6 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1288,6 +1288,8 @@ mod remix_tests; #[cfg(test)] mod ripple_delete_tests; #[cfg(test)] +mod ripple_head_tests; +#[cfg(test)] mod roll_linked_tests; #[cfg(test)] mod scopes_tests; diff --git a/crates/engine/src/ripple_head_tests.rs b/crates/engine/src/ripple_head_tests.rs new file mode 100644 index 00000000..7661d206 --- /dev/null +++ b/crates/engine/src/ripple_head_tests.rs @@ -0,0 +1,88 @@ +//! #1357: `edit.rippleDeleteHead` removes the portion of the selected clip from its in point +//! through the playhead and ripples the sequence closed. + +use crate::Session; +use filmcraft_project::ClipId; +use filmcraft_time::Tick; +use serde_json::json; + +fn f(s: &Session, n: i64) -> Tick { + s.sequence_rate().tick_of(n) +} + +fn range(s: &Session, c: ClipId) -> (Tick, Tick) { + s.active_sequence().unwrap().find_item(c).map(|(_, i)| (i.start, i.end())).unwrap() +} + +/// Four 48-frame clips in a row, pictures on V1 and their linked sounds on A1: [(picture, sound); 4]. +fn four_clips(s: &mut Session) -> [(ClipId, ClipId); 4] { + s.execute("file.openDemoProject", json!({})).unwrap(); + s.execute("file.newSequence", json!({"name": "Head", "video": 1, "audio": 1})).unwrap(); + let rate = s.sequence_rate(); + let item = s.project.items.values().find(|i| i.has_video() && i.has_audio() && i.as_media().is_some()).map(|i| i.id).unwrap(); + for n in 0..4 { + s.execute("timeline.place", json!({"item": item.0, "frame": 48 * n, "sourceIn": rate.tick_of(48).0, "duration": rate.tick_of(48).0})).unwrap(); + } + let q = s.active_sequence().unwrap(); + [0, 1, 2, 3].map(|n| (q.video_tracks[0].items[n].id, q.audio_tracks[0].items[n].id)) +} + +/// The head (in point .. playhead) goes and the rest of the sequence closes up: the clip keeps its +/// tail at the left of its old range, later clips shift left. +#[test] +fn ripple_delete_head_closes_the_head_and_shifts_later_clips() { + let mut s = Session::default(); + let [(_, _), (pic2, snd2), (pic3, snd3), (pic4, snd4)] = four_clips(&mut s); + s.set_playhead(f(&s, 60)); + s.state.selection = vec![pic2]; + + s.execute("edit.rippleDeleteHead", json!({})).unwrap(); + let q = s.active_sequence().unwrap(); + q.check().unwrap(); + assert_eq!(range(&s, pic2), (f(&s, 48), f(&s, 84)), "the head is gone and the tail slid left"); + assert_eq!(range(&s, snd2), (f(&s, 48), f(&s, 84)), "the linked sound follows"); + assert_eq!(range(&s, pic3), (f(&s, 84), f(&s, 132)), "the next picture closes up by the head length"); + assert_eq!((range(&s, snd3), range(&s, pic4), range(&s, snd4)), ((f(&s, 84), f(&s, 132)), (f(&s, 132), f(&s, 180)), (f(&s, 132), f(&s, 180)))); + assert_eq!(q.duration(), f(&s, 180), "the sequence is 12 frames shorter"); +} + +/// The trimmed clip's media in point advances by the head length (the head frames are dropped, +/// not hidden). +#[test] +fn ripple_delete_head_advances_the_source_in_point() { + let mut s = Session::default(); + let [(pic1, _), ..] = four_clips(&mut s); + let in_before = s.active_sequence().unwrap().find_item(pic1).unwrap().1.source_in; + s.set_playhead(f(&s, 12)); + s.state.selection = vec![pic1]; + s.execute("edit.rippleDeleteHead", json!({})).unwrap(); + let item = s.active_sequence().unwrap().find_item(pic1).unwrap().1; + assert_eq!(item.source_in, in_before + f(&s, 12), "the head frames are dropped from the source"); + assert_eq!((item.start, item.end()), (Tick::ZERO, f(&s, 36)), "the clip starts at zero, 36 frames long"); +} + +/// The playhead at or before the in point has no head to remove. +#[test] +fn ripple_delete_head_needs_the_playhead_past_the_in_point() { + let mut s = Session::default(); + let [(pic1, _), ..] = four_clips(&mut s); + s.set_playhead(Tick::ZERO); + s.state.selection = vec![pic1]; + s.execute("edit.rippleDeleteHead", json!({})).unwrap_err(); + assert_eq!(range(&s, pic1), (Tick::ZERO, s.sequence_rate().tick_of(48)), "nothing moved"); +} + +/// The playhead past the clip's out point deletes the whole clip (the head covers all of it). +#[test] +fn ripple_delete_head_past_the_end_removes_the_whole_clip() { + let mut s = Session::default(); + let [(_, _), (pic2, snd2), (pic3, snd3), ..] = four_clips(&mut s); + s.set_playhead(f(&s, 120)); // past clip 2's out point at 96 + s.state.selection = vec![pic2]; + s.execute("edit.rippleDeleteHead", json!({})).unwrap(); + let q = s.active_sequence().unwrap(); + q.check().unwrap(); + assert!(q.find_item(pic2).is_none() && q.find_item(snd2).is_none(), "the whole clip went"); + assert_eq!(range(&s, pic3), (f(&s, 48), f(&s, 96)), "later clips closed the whole 48-frame gap"); + assert_eq!(range(&s, snd3), (f(&s, 48), f(&s, 96))); +} From 1ef2960514906ec873413d59678c386cd55dceed Mon Sep 17 00:00:00 2001 From: bfenty Date: Mon, 12 Oct 2026 21:15:13 +0000 Subject: [PATCH 09/18] h264: validate PPS fields before arithmetic; conceal weighted_bipred_idc 3 (#1441) --- crates/h264/src/params.rs | 31 ++++-- crates/h264/tests/pps_validation.rs | 142 ++++++++++++++++++++++++++++ pr1441.md | 18 ++++ 3 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 crates/h264/tests/pps_validation.rs create mode 100644 pr1441.md diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs index 51ca1fd2..dd7b4d23 100644 --- a/crates/h264/src/params.rs +++ b/crates/h264/src/params.rs @@ -569,8 +569,9 @@ impl Pps { let sps = spss.get(sps_id as usize).and_then(|s| s.as_ref()).ok_or_else(|| crate::Error::MissingParameterSet(format!("SPS {sps_id} for PPS {id}")))?; let entropy_coding_mode = r.read_flag()?; let bottom_field_pic_order_in_frame_present = r.read_flag()?; - let num_slice_groups = r.read_ue()? + 1; - ensure!(num_slice_groups <= 8, "num_slice_groups out of range"); + let num_slice_groups_minus1 = r.read_ue()?; + ensure!(num_slice_groups_minus1 < 8, "num_slice_groups out of range"); + let num_slice_groups = num_slice_groups_minus1 + 1; let mut slice_group_map_type = 0; if num_slice_groups > 1 { // Parsed for completeness; FMO decoding is unsupported. @@ -592,7 +593,9 @@ impl Pps { r.read_ue()?; } 6 => { - let n = r.read_ue()? + 1; + let n_minus1 = r.read_ue()?; + ensure!(n_minus1 <= 128, "slice_group_run_length out of range"); + let n = n_minus1 + 1; let bits = 32 - (num_slice_groups - 1).leading_zeros(); for _ in 0..n { r.read_bits(bits)?; @@ -601,13 +604,23 @@ impl Pps { _ => {} } } - let num_ref_idx_l0_default_active = r.read_ue()? + 1; - let num_ref_idx_l1_default_active = r.read_ue()? + 1; - ensure!(num_ref_idx_l0_default_active <= 32 && num_ref_idx_l1_default_active <= 32, "num_ref_idx_default_active out of range"); + let num_ref_idx_l0_minus1 = r.read_ue()?; + let num_ref_idx_l1_minus1 = r.read_ue()?; + ensure!(num_ref_idx_l0_minus1 < 32 && num_ref_idx_l1_minus1 < 32, "num_ref_idx_default_active out of range"); + let num_ref_idx_l0_default_active = num_ref_idx_l0_minus1 + 1; + let num_ref_idx_l1_default_active = num_ref_idx_l1_minus1 + 1; let weighted_pred = r.read_flag()?; - let weighted_bipred_idc = r.read_bits(2)?; - let pic_init_qp = 26 + r.read_se()?; - let pic_init_qs = 26 + r.read_se()?; + // 7.4.2.2.1 fall-back: a value of 3 shall be considered equal to 2. + let weighted_bipred_idc = (r.read_bits(2)?).min(2); + // 7.4.2.2: pic_init_qp_minus26 in -(26 + QpBdOffsetY)..=25, pic_init_qs_minus26 in -26..=25. + let qp_bd_offset_y = 6 * (sps.bit_depth_luma.saturating_sub(8)) as i32; + let qp_min = -(26 + qp_bd_offset_y); + let pic_init_qp_minus26 = r.read_se()?; + ensure!((qp_min..=25).contains(&pic_init_qp_minus26), "pic_init_qp_minus26 out of range"); + let pic_init_qs_minus26 = r.read_se()?; + ensure!((-26..=25).contains(&pic_init_qs_minus26), "pic_init_qs_minus26 out of range"); + let pic_init_qp = 26 + pic_init_qp_minus26; + let pic_init_qs = 26 + pic_init_qs_minus26; let chroma_qp_index_offset = r.read_se()?; ensure!((-12..=12).contains(&chroma_qp_index_offset), "chroma_qp_index_offset out of range"); let deblocking_filter_control_present = r.read_flag()?; diff --git a/crates/h264/tests/pps_validation.rs b/crates/h264/tests/pps_validation.rs new file mode 100644 index 00000000..d5ed96e9 --- /dev/null +++ b/crates/h264/tests/pps_validation.rs @@ -0,0 +1,142 @@ +//! #1441: malformed PPS fields must return `Error::Invalid`, not panic. + +use filmcraft_h264::Decoder; + +/// Valid, synthetic Baseline SPS (176x144, 8-bit). +const SPS: [u8; 12] = [0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1e, 0xda, 0x0b, 0x13, 0x90]; + +fn decode_pps(pps: &[u8]) -> Result<(), filmcraft_h264::Error> { + let mut decoder = Decoder::with_threads(1); + decoder.decode(&SPS, 0).expect("sps decodes"); + decoder.decode(pps, 0).map(|_| ()) +} + +/// The reporter's literal: PPS with `num_ref_idx_l0_default_active_minus1 = u32::MAX`. +#[test] +fn hostile_pps_returns_an_error_instead_of_panicking() { + let pps = [0x00, 0x00, 0x00, 0x01, 0x68, 0xc8, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0x02, 0x3c, 0x80]; + let outcome = std::panic::catch_unwind(|| { + let mut decoder = Decoder::with_threads(1); + decoder.decode(&SPS, 0).unwrap(); + decoder.decode(&pps, 0) + }); + assert!(outcome.is_ok_and(|result| result.is_err())); +} + +/// Write ue(v) as bits (helper for building synthetic PPS payloads). +fn push_ue(bits: &mut Vec, v: u32) { + let x = v as u64 + 1; + let len = 64 - x.leading_zeros(); + for _ in 1..len { + bits.push(false); + } + for i in (0..len).rev() { + bits.push(x & (1 << i) != 0); + } +} + +fn push_bits(bits: &mut Vec, v: u32, n: u32) { + for i in (0..n).rev() { + bits.push(v & (1 << i) != 0); + } +} + +fn to_bytes(bits: &[bool]) -> Vec { + let mut out = Vec::new(); + for chunk in bits.chunks(8) { + let mut b = 0u8; + for (i, v) in chunk.iter().enumerate() { + b |= (*v as u8) << (7 - i); + } + out.push(b); + } + out +} + +/// Build a PPS NAL (header byte 0x68 + payload) with `num_slice_groups_minus1` = u32::MAX. +#[test] +fn hostile_num_slice_groups_is_rejected() { + let mut bits = vec![true, true, false, false]; + push_ue(&mut bits, u32::MAX); // num_slice_groups_minus1 + bits.extend(std::iter::repeat_n(false, 64)); + let payload = to_bytes(&bits); + bits.extend(std::iter::repeat_n(false, 64)); + let mut nal = vec![0x00, 0x00, 0x00, 0x01, 0x68]; + nal.extend_from_slice(&payload); + assert!(std::panic::catch_unwind(|| decode_pps(&nal)).is_ok_and(|r| r.is_err())); +} + +/// `num_ref_idx_l1_default_active_minus1 = u32::MAX` (l0 valid) must error, not panic. +#[test] +fn hostile_num_ref_idx_l1_is_rejected() { + let mut bits = vec![true, true, false, false]; + push_ue(&mut bits, 0); // num_slice_groups_minus1 + push_ue(&mut bits, 1); // num_ref_idx_l0_minus1 + push_ue(&mut bits, u32::MAX); // num_ref_idx_l1_minus1 + bits.extend(std::iter::repeat_n(false, 64)); + let mut nal = vec![0x00, 0x00, 0x00, 0x01, 0x68]; + nal.extend_from_slice(&to_bytes(&bits)); + assert!(std::panic::catch_unwind(|| decode_pps(&nal)).is_ok_and(|r| r.is_err())); +} + +/// `pic_init_qp_minus26 = i32::MAX` must error, not overflow. +#[test] +fn hostile_pic_init_qp_is_rejected() { + let mut bits = vec![true, true, false, false]; + push_ue(&mut bits, 0); // num_slice_groups_minus1 + push_ue(&mut bits, 0); // num_ref_idx_l0_minus1 + push_ue(&mut bits, 0); // num_ref_idx_l1_minus1 + bits.push(false); // weighted_pred + push_bits(&mut bits, 0, 2); // weighted_bipred_idc + // se(i32::MAX): codeNum = 2*MAX-1 = u32::MAX-2... encode via ue of huge codeNum + push_ue(&mut bits, u32::MAX - 2); // codeNum for +2^31-1... any out-of-range codeNum works + bits.extend(std::iter::repeat_n(false, 64)); + let mut nal = vec![0x00, 0x00, 0x00, 0x01, 0x68]; + nal.extend_from_slice(&to_bytes(&bits)); + assert!(std::panic::catch_unwind(|| decode_pps(&nal)).is_ok_and(|r| r.is_err())); +} + +/// `weighted_bipred_idc = 3` is concealed to 2 per 7.4.2.2.1 (not rejected, not a panic). +#[test] +fn reserved_weighted_bipred_idc_is_concealed() { + let mut bits = vec![true, true, false, false]; + push_ue(&mut bits, 0); + push_ue(&mut bits, 0); + push_ue(&mut bits, 0); + bits.push(false); // weighted_pred + push_bits(&mut bits, 3, 2); // weighted_bipred_idc = 3 (reserved) + push_ue(&mut bits, 0); // pic_init_qp_minus26 = 0 + push_ue(&mut bits, 0); // pic_init_qs_minus26 = 0 + push_ue(&mut bits, 0); // chroma_qp_index_offset = 0 + bits.push(false); // deblocking_filter_control_present + bits.push(false); // constrained_intra_pred + bits.push(false); // redundant_pic_cnt_present + bits.extend(std::iter::repeat_n(false, 64)); + let mut nal = vec![0x00, 0x00, 0x00, 0x01, 0x68]; + nal.extend_from_slice(&to_bytes(&bits)); + // parses fine; the decoder treats the field as 2 + decode_pps(&nal).expect("weighted_bipred_idc=3 concealed to 2"); +} + +/// A well-formed minimal PPS still parses (no regression). +#[test] +fn minimal_valid_pps_parses() { + let mut bits = vec![true, true, false, false]; + push_ue(&mut bits, 0); + push_ue(&mut bits, 0); + push_ue(&mut bits, 0); + bits.push(false); // weighted_pred + push_bits(&mut bits, 0, 2); // weighted_bipred_idc + push_ue(&mut bits, 0); // pic_init_qp_minus26 + push_ue(&mut bits, 0); // pic_init_qs_minus26 + push_ue(&mut bits, 0); // chroma_qp_index_offset + bits.push(true); // deblocking_filter_control_present + bits.push(false); // constrained_intra_pred + bits.push(false); // redundant_pic_cnt_present + bits.extend(std::iter::repeat_n(false, 64)); + let mut nal = vec![0x00, 0x00, 0x00, 0x01, 0x68]; + nal.extend_from_slice(&to_bytes(&bits)); + if let Err(e) = decode_pps(&nal) { + panic!("minimal PPS rejected: {e}"); + } +} diff --git a/pr1441.md b/pr1441.md new file mode 100644 index 00000000..f3e0ea34 --- /dev/null +++ b/pr1441.md @@ -0,0 +1,18 @@ +## What + +`Pps::parse` added offsets to input-derived fields before validating them, so six malformed-PPS fields panic (overflow checks on) instead of returning `Error::Invalid`. The panic reaches the public `Decoder::decode` API. Fixes #1441. + +## Changes (`crates/h264/src/params.rs`) + +- **Validate before incrementing**: `num_slice_groups_minus1 < 8`, `num_ref_idx_l0/l1_default_active_minus1 < 32` are checked pre-`+1`; the map-type-6 run-length count is bounded before `+1`. +- **QP/QS range checks per 7.4.2.2**: `pic_init_qp_minus26` must be in `-(26 + QpBdOffsetY)..=25` (bit-depth aware via the SPS) and `pic_init_qs_minus26` in `-26..=25`, checked before adding 26. +- **`weighted_bipred_idc`**: concealed `3 → 2` per the 7.4.2.2.1 fall-back rule ("shall be considered equal to 2") rather than rejected — matches the spec text, and the slice decoder already only distinguishes 0/1/2. + +## Tests (`crates/h264/tests/pps_validation.rs`, 6 tests) + +- The reporter's literal 12-byte SPS + 18-byte PPS: `catch_unwind` now sees `Err`, not a panic. +- Synthetic bit-built PPSes for hostile `num_slice_groups`, hostile `num_ref_idx_l1`, hostile `pic_init_qp`. +- `weighted_bipred_idc = 3` parses with the field concealed to 2. +- A well-formed minimal PPS still parses (control). + +Red/green verified: reporter-literal and concealment tests fail on main (panic at params.rs:604 / silent accept), all 6 pass with the fix; negative control via stash round-trip. Full `filmcraft-h264` suite green (7 test binaries, 0 failures), `cargo fmt --check` and `cargo clippy -p filmcraft-h264 --all-targets` clean. From b97bb74330c35f0145e8abded296d254be769fbf Mon Sep 17 00:00:00 2001 From: CokeRoach Date: Tue, 13 Oct 2026 08:44:01 +0900 Subject: [PATCH 10/18] i18n: Korean (ko) interface catalog (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Korean as an interface language: Edit ▸ Language ▸ 한국어, Settings ▸ General ▸ Interface Language, and System Language for ko / ko-KR locales. - crates/ui-egui/src/i18n/ko.tsv: 3,573 entries, the Spanish catalog's sources plus the macOS Settings item, written clean-room from the plain meaning of the English text (sidecar ko.tsv.attribution, ATTRIBUTION.md row). - Hangul is drawn with the installed system face cjk.rs already adds (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); switching is refused with a message when there is none, like Chinese. - Test: korean_covers_the_menus_and_has_no_dead_entries. - Docs: localization-parity, ROADMAP, ui-egui README. Native-speaker review welcome. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XWUReif4gqcjU5GsjYioc --- ATTRIBUTION.md | 1 + ROADMAP.md | 1 + apps/filmcraft/src/native_menu.rs | 2 + crates/engine/src/settings.rs | 1 + crates/ui-egui/README.md | 2 +- crates/ui-egui/src/cjk.rs | 6 + crates/ui-egui/src/i18n/ko.tsv | 3583 ++++++++++++++++++++ crates/ui-egui/src/i18n/ko.tsv.attribution | 8 + crates/ui-egui/src/i18n/mod.rs | 46 +- crates/ui-egui/src/menus.rs | 8 + docs/localization-parity.md | 4 +- 11 files changed, 3657 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/src/i18n/ko.tsv create mode 100644 crates/ui-egui/src/i18n/ko.tsv.attribution diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 35622be4..1483c910 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -10,6 +10,7 @@ iconography, images or other Adobe assets; open licences only; every asset attri | Asset | Author | Source | Licence | |---|---|---|---| | `crates/ui-egui/src/i18n/id.tsv` | FilmCraft contributors | Original work: clean-room Indonesian interface translations | MIT OR Apache-2.0 | +| `crates/ui-egui/src/i18n/ko.tsv` | FilmCraft contributors | Original work: clean-room Korean interface translations | MIT OR Apache-2.0 | | `crates/ui-egui/src/i18n/fr.tsv` | FilmCraft contributors | Original work: clean-room French interface translations | MIT OR Apache-2.0 | | `crates/ui-egui/src/i18n/pt-br.tsv` | FilmCraft contributors | Original work: clean-room Brazilian Portuguese interface translations | MIT OR Apache-2.0 | | `crates/ui-egui/src/i18n/ru.tsv` | FilmCraft contributors | Original work: clean-room Russian interface translations | MIT OR Apache-2.0 | diff --git a/ROADMAP.md b/ROADMAP.md index 7f3962fd..ca990c57 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -84,6 +84,7 @@ Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings | French | partial (whole interface; native review welcome) | ~100% | | Portuguese (Brazil) | partial (whole interface; native review welcome) | ~100% | | Indonesian | partial (whole interface; native review welcome) | ~100% | +| Korean | partial (whole interface; needs an installed Hangul font; native review welcome) | ~100% | | Japanese | partial (whole interface) | 99.7% | | German | menus only (native-reviewed) | 10.6% | | Korean | none | 0% | diff --git a/apps/filmcraft/src/native_menu.rs b/apps/filmcraft/src/native_menu.rs index 8ac83069..4f8d0228 100644 --- a/apps/filmcraft/src/native_menu.rs +++ b/apps/filmcraft/src/native_menu.rs @@ -131,6 +131,8 @@ pub fn install(app: &FilmcraftApp, ctx: egui::Context) -> (Receiver, Men filmcraft_ui_egui::i18n::Language::Fr } else if checked("app.language.indonesian") { filmcraft_ui_egui::i18n::Language::Id + } else if checked("app.language.korean") { + filmcraft_ui_egui::i18n::Language::Ko } else { filmcraft_ui_egui::i18n::Language::En }; diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs index 6e3cccdf..55abc85e 100644 --- a/crates/engine/src/settings.rs +++ b/crates/engine/src/settings.rs @@ -1007,6 +1007,7 @@ static CATEGORIES: &[Category] = &[ ("ru", "Русский"), ("fr", "Français"), ("id", "Bahasa Indonesia"), + ("ko", "한국어"), ]), true, ), diff --git a/crates/ui-egui/README.md b/crates/ui-egui/README.md index cb618161..bb820016 100644 --- a/crates/ui-egui/README.md +++ b/crates/ui-egui/README.md @@ -18,7 +18,7 @@ the committed span and undo. Set `FILMCRAFT_UI_SNAPSHOT_DIR` to render screensho ## Localisation Edit > Language offers English, Japanese, Spanish, Brazilian Portuguese, Ukrainian, Simplified -Chinese, German, Russian, French and Indonesian (which covers the whole interface). The language is stored in the engine's `general.interfaceLanguage` preference, +Chinese, German, Russian, French, Indonesian and Korean (which cover the whole interface). The language is stored in the engine's `general.interfaceLanguage` preference, restored on startup, and also available in Settings > General. Its default, System Language (`system`), follows the operating system: the first of the user's preferred languages that the interface has (the host supplies them through diff --git a/crates/ui-egui/src/cjk.rs b/crates/ui-egui/src/cjk.rs index 82b6bab6..ab1345b2 100644 --- a/crates/ui-egui/src/cjk.rs +++ b/crates/ui-egui/src/cjk.rs @@ -55,6 +55,12 @@ fn find_system_font(sample: &str, preferred: &[&str]) -> Option> { /// Whether [`install`] has a Chinese face to add: craft-fonts Hans/Hant faces, or an installed /// system face (scanned once per process). The Simplified Chinese interface needs one. +/// Whether [`install`] has a Hangul face to add (an installed system face; none is bundled). +/// The Korean interface needs one. +pub(crate) fn korean_available() -> bool { + korean_font().is_some() +} + pub(crate) fn available() -> bool { craft_fonts().next().is_some() || system_font().is_some() } diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv new file mode 100644 index 00000000..d88aa57a --- /dev/null +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -0,0 +1,3583 @@ +# Korean (ko) catalog. Columns, separated by a TAB: context source translation +# (format documented in the header of es.tsv). +# +# Clean-room: every entry was written from the plain meaning of the English text using ordinary +# Korean video-editing vocabulary (시퀀스, 타임라인, 트랙, 클립, 재생 헤드, 빈, 렌더, 내보내기…). No +# proprietary localisation resource, string table or product UI was consulted or copied; add entries +# the same way. Product and technology names (FilmCraft, ArtCraft, Lumetri, ProRes, LUFS…) keep their +# spelling. Hangul is drawn with an installed system font (Malgun Gothic, Apple SD Gothic Neo, Noto +# Sans CJK KR…, see cjk.rs); native-speaker review welcome. + + Angle 각도 + Blur 흐림 + Corner Radius 모서리 반경 + Distance 거리 + Opacity 불투명도 + Size 크기 + Template: {name} 템플릿: {name} + (none) (없음) + (removed from {list}) ({list}에서 제거됨) + (unavailable) (사용 불가) + ({ch} output channels) (출력 채널 {ch}개) + and 및 + frames 프레임 + seconds 초 + · {time} left · {time} 남음 + % of all commits 전체 커밋 중 % + (Custom) (사용자 정의) + (Default) (기본값) + (empty) (비어 있음) + (gap) (간격) + (no audio) (오디오 없음) + (no clip selected) (선택된 클립 없음) + (no clips) (클립 없음) + (no sequence) (시퀀스 없음) + (no sequences) (시퀀스 없음) + (RGB) Gamma (RGB) 감마 + (RGB) Input Black Level (RGB) 입력 검정 레벨 + (RGB) Input White Level (RGB) 입력 흰색 레벨 + (RGB) Output Black Level (RGB) 출력 검정 레벨 + (RGB) Output White Level (RGB) 출력 흰색 레벨 + + Cmd + Cmd + + Opt + Opt + 0 fastest, 8 smallest 0 가장 빠름, 8 가장 작음 + 1 file 파일 1개 + 10 Bit Black Point 10비트 검정 점 + 10 Bit White Point 10비트 흰색 점 + 105mm Prime 105mm 단초점 + 16 bit 16비트 + 24 bit 24비트 + 29.97 fps Drop-Frame 29.97 fps 드롭 프레임 + 29.97 fps Non Drop-Frame 29.97 fps 논드롭 프레임 + 35mm Prime 35mm 단초점 + 3D Roll 3D 롤 + 3D Rotate 3D 회전 + 3D Spin 3D 스핀 + 3D Spinback 3D 역스핀 + 4-Color Gradient 4색 그라디언트 + 5.1 Media 5.1 미디어 + 5.1 Mixdown Type 5.1 믹스다운 유형 + 5.1 panner (drag the puck; double-click: front centre) 5.1 패너(퍽 드래그, 더블클릭: 전면 중앙) + 50-300mm Zoom 50-300mm 줌 + 59.94 fps Drop-Frame 59.94 fps 드롭 프레임 + 59.94 fps Non Drop-Frame 59.94 fps 논드롭 프레임 + A boxed label with a round pointer; move the label and the pointer follows. 둥근 포인터가 있는 상자 레이블. 레이블을 옮기면 포인터가 따라갑니다. + A clean-room, pure-Rust non-linear video editor. Part of the ArtCraft family. 클린룸 방식의 순수 Rust 비선형 비디오 편집기. ArtCraft 제품군의 일부입니다. + A cut sequence with transitions, effects and music 전환, 효과, 음악이 포함된 편집 시퀀스 + A file dialog is already open 파일 대화상자가 이미 열려 있습니다 + A news-style line that crawls in from the right and leaves at the left. 오른쪽에서 흘러 들어와 왼쪽으로 사라지는 뉴스형 자막. + A rounded tag that fits its text, with a status dot. 텍스트에 맞는 둥근 태그와 상태 점. + A title on a rounded card with a soft shadow. 부드러운 그림자가 있는 둥근 카드 위 제목. + AAC Audio AAC 오디오 + AAC Audio 48 kHz 320 kbps AAC 오디오 48 kHz 320 kbps + AAF Export Settings AAF 내보내기 설정 + AAF… AAF… + About 정보 + About FilmCraft FilmCraft 정보 + Accent Color 강조 색상 + Accessible color contrast 접근성 색상 대비 + Action and Title Safe Areas 액션 및 제목 안전 영역 + Action Safe Area 액션 안전 영역 + Action-Safe Margin 액션 세이프 여백 + Action: 동작: + Active 활성 + Active Monitor 활성 모니터 + Adaptive 적응형 + Adaptive ({bpp} bits per pixel) 적응형(픽셀당 {bpp}비트) + Add 추가 + Add a timecode row 타임코드 행 추가 + Add another shortcut 단축키 추가 + Add Audio Submix Track 오디오 서브믹스 트랙 추가 + Add audio submix tracks 오디오 서브믹스 트랙 추가 + Add audio tracks 오디오 트랙 추가 + Add Caption at Playhead 재생 헤드 위치에 자막 추가 + Add caption at playhead 재생 헤드에 자막 추가 + Add captions 자막 추가 + Add Chapter Marker… 챕터 마커 추가… + Add Edit 편집점 추가 + Add Edit to All Tracks 모든 트랙에 편집점 추가 + Add Flash Cue Marker… 플래시 큐 마커 추가… + Add Frame Hold 프레임 고정 추가 + Add Guide 안내선 추가 + Add Guide… 안내선 추가… + Add keyframe 키프레임 추가 + Add Lumetri Color to clip 클립에 Lumetri Color 추가 + Add Marker 마커 추가 + Add Marker (M) 마커 추가(M) + Add media to your project. Drop files anywhere in the window, or choose a source below. 프로젝트에 미디어를 추가합니다. 창 아무 곳에나 파일을 놓거나 아래에서 소스를 선택하세요. + Add New Caption Track… 새 자막 트랙 추가… + Add Property… 속성 추가… + Add Range Marker 범위 마커 추가 + Add Range Marker to In and Out 시작/끝에 범위 마커 추가 + Add to Favorites 즐겨찾기에 추가 + Add to favorites 즐겨찾기에 추가 + Add Tracks 트랙 추가 + Add tracks automatically when editing source clips onto the Timeline 소스 클립을 타임라인에 편집할 때 트랙 자동 추가 + Add Tracks… 트랙 추가… + Add video tracks 비디오 트랙 추가 + Add/Remove Keyframe 키프레임 추가/제거 + Added to the export queue 내보내기 대기열에 추가됨 + Additional Scale 추가 비율 + Additive Dissolve 더하기 디졸브 + Additive Dissolve (Legacy) 더하기 디졸브(레거시) + Adjust 조정 + Adjust Gain by: 게인 조정: + Adjustment Layer… 조정 레이어… + Adjustments 조정 + Advanced 고급 + After {name} {name} 뒤 + Aggressive 강하게 + AIFF AIFF + AIFF 48 kHz 16-bit AIFF 48 kHz 16비트 + AIFF, stereo 48 kHz 16-bit PCM AIFF, 스테레오 48 kHz 16비트 PCM + Align 정렬 + Align and Transform 정렬 및 변형 + Align Bottom 아래쪽 정렬 + Align center 가운데 정렬 + Align Center Horizontally 가로 가운데 정렬 + Align Center Vertically 세로 가운데 정렬 + Align Left 왼쪽 정렬 + Align left 왼쪽 정렬 + Align Right 오른쪽 정렬 + Align right 오른쪽 정렬 + Align Timecode 타임코드 맞추기 + Align to Grid 격자에 맞춤 + Align to Selection 선택 영역에 정렬 + Align to Video Frame 비디오 프레임에 정렬 + Align to Video Frame as Group 그룹으로 비디오 프레임에 정렬 + Align Top 위쪽 정렬 + Alignment 정렬 + All 모두 + All Cameras 모든 카메라 + All Caps 모두 대문자 + All Categories 모든 범주 + All Empty Tracks 모든 빈 트랙 + All Events 모든 이벤트 + All lines 모든 줄 + All markers are hidden by the colour filter. 색상 필터로 모든 마커가 숨겨졌습니다. + All media is online. 모든 미디어가 온라인입니다. + All Panels 모든 패널 + Allow current tool to change trim type of previously selected edit point 현재 도구로 이전 선택 편집점의 트림 유형 변경 허용 + Allow Duplicate Media During Project Import 프로젝트 가져오기 시 중복 미디어 허용 + Allow duplicate media during project import 프로젝트 가져오기 시 중복 미디어 허용 + Allow Selection tool to choose Roll and Ripple trims without modifier key 수정 키 없이 선택 도구로 롤 및 리플 트림 선택 허용 + Alpha 알파 + Alpha Adjust 알파 조정 + Alpha Blurriness 알파 흐림 + Alpha Channel 알파 채널 + Alpha Glow 알파 광선 + Always Deinterlace 항상 디인터레이스 + Always import embedded CEA-708 closed captions with wide aspect. 내장 CEA-708 자막을 항상 와이드 비율로 가져오기 + Always override audio tags 항상 오디오 태그 재정의 + Ambience 앰비언스 + Ambience Intensity 앰비언스 강도 + Ambience Light Color 앰비언스 조명 색상 + Amount 수량 + Amount of Noise 노이즈 양 + Amount to Decolor 색상 제거 양 + Amount to Tint 색조 입히기 양 + Amplifier 앰프 + Amplify 증폭 + Amplitude 진폭 + Amplitude and Compression 진폭 및 압축 + An earlier start failed with the default graphics. Updating the graphics driver usually fixes this. 이전에 기본 그래픽으로 시작하지 못했습니다. 그래픽 드라이버를 업데이트하면 대개 해결됩니다. + Analog 아날로그 + Analog Delay 아날로그 지연 + Analyze 분석 + Analyze all imported media to search for visuals or audio 가져온 모든 미디어를 분석해 영상 또는 오디오 검색 + Anamorphic 아나모픽 + Anamorphic 2:1 (2.0) 아나모픽 2:1(2.0) + Anchor 기준 + Anchor Point 기준점 + Anchor point 기준점 + Anchor to In Point 시작 표시(In)에 고정 + Anchor to Out Point 끝 표시(Out)에 고정 + Angle 각도 + Animated GIF 애니메이션 GIF + Animated GIF 640×360 애니메이션 GIF 640×360 + Animation 애니메이션 + Anti-aliasing Quality 앤티앨리어싱 품질 + Anti-flicker Filter 깜박임 방지 필터 + Antialiasing for Best Quality 최고 품질용 앤티앨리어싱 + Any 하나 이상 + Appearance 모양 + Appearance Mode 모양 모드 + Appearance Settings… 모양 설정… + Appearance… 모양… + Apple ProRes Apple ProRes + Apple ProRes 422 Apple ProRes 422 + Apple ProRes 422 HQ Apple ProRes 422 HQ + Apple ProRes 422 LT Apple ProRes 422 LT + Apple ProRes 422 Proxy Apple ProRes 422 Proxy + Apple ProRes 4444 Apple ProRes 4444 + Apple ProRes 4444 with Alpha Apple ProRes 4444(알파 포함) + Apple ProRes 4444 XQ Apple ProRes 4444 XQ + Application 응용 프로그램 + Application shortcuts work whatever panel has focus 응용 프로그램 단축키는 어느 패널에서나 작동 + Apply 적용 + Apply a cut at each detected cut point 감지된 각 컷 지점에서 자르기 + Apply Audio Transition 오디오 전환 적용 + Apply Default Audio Transition 기본 오디오 전환 적용 + Apply Default Transitions 기본 전환 적용 + Apply Default Transitions to Selection 선택 항목에 기본 전환 적용 + Apply Default Transitions to Selection (Shift+D) 선택 항목에 기본 전환 적용(Shift+D) + Apply Default Video Transition 기본 비디오 전환 적용 + Apply Guide Template 안내선 템플릿 적용 + Apply Match 일치 적용 + Apply to Selected Clips 선택한 클립에 적용 + Apply Video Transition 비디오 전환 적용 + APV APV + APV 422-10 APV 422-10 + APV 422-12 APV 422-12 + Are you sure you want to discard your changes to '{file}'? '{file}'의 변경 사항을 버릴까요? + Arrange 배치 + ArtCraft Website ArtCraft 웹사이트 + ASC CDL ASC CDL + Aspect 종횡비 + Assembly 어셈블리 + Assign an audio type to the selection: 선택 항목에 오디오 유형 지정: + Assign {key} to {command} {key}을(를) {command}에 할당 + Asymmetry 비대칭 + At playback end, return to beginning when restarting playback 재생 끝에서 다시 재생 시 처음으로 돌아가기 + At Startup 시작 시 + At Unnumbered Markers 번호 없는 마커에 + Att 어택 + Attach Proxies… 프록시 연결… + Attack 어택 + Attempt to force hardware to document sample rate 하드웨어를 문서 샘플 속도로 강제 시도 + Audio 오디오 + Audio analysis: 오디오 분석: + Audio Attributes 오디오 속성 + Audio Bitrate 오디오 비트 전송률 + Audio Channels… 오디오 채널… + Audio Clip Mixer 오디오 클립 믹서 + Audio Codec 오디오 코덱 + Audio Duration 오디오 길이 + Audio Effects 오디오 효과 + Audio effects 오디오 효과 + Audio File Format: 오디오 파일 형식: + Audio Format 오디오 형식 + Audio Gain 오디오 게인 + Audio Gain… 오디오 게인… + Audio Hardware 오디오 하드웨어 + Audio Hardware… 오디오 하드웨어… + Audio In 오디오 입력 + Audio In Point 오디오 시작 표시(In) + Audio Info 오디오 정보 + Audio Meters 오디오 미터 + Audio on track {t} 트랙 {t}의 오디오 + Audio Only 오디오만 + Audio only 오디오만 + Audio Options 오디오 옵션 + Audio Out 오디오 출력 + Audio Out Point 오디오 끝 표시(Out) + Audio output failed: playing without sound (check Settings ▸ Audio Hardware) 오디오 출력 실패: 소리 없이 재생 중 (설정 ▸ 오디오 하드웨어 확인) + Audio output is not responding: playing without sound (check Settings ▸ Audio Hardware) 오디오 출력 응답 없음: 소리 없이 재생 중 (설정 ▸ 오디오 하드웨어 확인) + Audio Previews: 오디오 미리 보기: + Audio Samples 오디오 샘플 + Audio Time Units 오디오 시간 단위 + Audio Track Mixer 오디오 트랙 믹서 + Audio Track Mixer menu 오디오 트랙 믹서 메뉴 + Audio Tracks 오디오 트랙 + Audio Transition Default Duration 오디오 전환 기본 길이 + Audio Transitions 오디오 전환 + Audio Usage 오디오 사용 + Audio Waveform 오디오 파형 + Audio Waveforms 오디오 파형 + Audio {n}: 오디오 {n}: + Audio: 오디오: + Audio… 오디오… + Auto 자동 + Auto (system) 자동 (시스템) + Auto Align 자동 정렬 + Auto Bezier 자동 베지어 + Auto Gate 자동 게이트 + Auto Makeup 자동 메이크업 + Auto Reframe 자동 리프레임 + Auto Save 자동 저장 + Auto Save also saves the current project(s) 자동 저장 시 현재 프로젝트도 저장 + Auto Save Now 지금 자동 저장 + Auto Save… 자동 저장… + Auto Tone Map Media (HDR and log into SDR) 미디어 자동 톤 매핑 (HDR 및 로그를 SDR로) + Auto tone map media (HDR and log into SDR) 미디어 자동 톤 매핑(HDR·log를 SDR로) + Auto-Adjust Multi-Camera Playback Quality 멀티 카메라 재생 품질 자동 조정 + Auto-Match 자동 일치 + Auto-Match Loudness 라우드니스 자동 일치 + Auto-saves go to: {dir} 자동 저장 위치: {dir} + Auto-tag audio types in the timeline 타임라인의 오디오 유형 자동 태그 + Auto-transcribe all imported clips 가져온 모든 클립 자동 전사 + Auto-transcribe only clips in sequences 시퀀스 내 클립만 자동 전사 + Automatch Time 자동 일치 시간 + Automate To Sequence 시퀀스로 자동화 + Automate to Sequence 시퀀스로 자동화 + Automate to Sequence… 시퀀스로 자동화… + Automatic 자동 + Automatic Click Remover 자동 클릭 제거 + Automatically delete cache files older than 다음보다 오래된 캐시 파일 자동 삭제 + Automatically delete oldest cache files when cache exceeds 캐시가 다음을 초과하면 가장 오래된 캐시 파일 자동 삭제 + Automatically Hide Dependent Clips 종속 클립 자동 숨기기 + Automatically refresh growing files 증가 중인 파일 자동 새로 고침 + Automatically resume playback for growing files in Source Monitor 소스 모니터에서 증가 중인 파일 재생 자동 재개 + Automatically Save Every 자동 저장 간격 + Automatically save projects 프로젝트 자동 저장 + Automatically transcribe clips 클립 자동 전사 + Automation Keyframe Optimization 자동화 키프레임 최적화 + AV1 AV1 + Average Data Rate 평균 데이터 속도 + Avid DNxHR Avid DNxHR + Avid DNxHR HQ Avid DNxHR HQ + Avid DNxHR LB Avid DNxHR LB + Avid DNxHR SQ Avid DNxHR SQ + Avid Log Exchange Avid Log Exchange + Avid Log Exchange… Avid Log Exchange… + Avid-style OP-Atom: DNxHR HQ picture file plus one 24-bit 48 kHz PCM file per channel (_A1, _A2…) Avid 방식 OP-Atom: DNxHR HQ 영상 파일 + 채널별 24비트 48 kHz PCM 파일(_A1, _A2…) + Background 배경 + Background Color 배경 색상 + Background Corner Radius 배경 모서리 반경 + Background fill 배경 채우기 + Background Opacity 배경 불투명도 + Background Size 배경 크기 + Balance 밸런스 + Balanced Voice 균형 잡힌 음성 + Band 1 밴드 1 + Band 2 밴드 2 + Band 3 밴드 3 + Band 4 밴드 4 + Band 5 밴드 5 + Band Pass 밴드 패스 + Band Slide 밴드 슬라이드 + Band Stop 밴드 제거 + Band {n} 밴드 {n} + Bandpass 대역 통과 + Bar Color 막대 색상 + Barn Doors 반도어 + Bars and Tone 컬러 바 및 톤 + Bars and Tone… 컬러 바 및 톤… + Baseline Baseline + Baseline Shift 기준선 이동 + Basic 기본 + Basic 3D 기본 3D + Basic Correction 기본 보정 + Bass 저음 + Before First Track 첫 트랙 앞 + Begin Text Editing for a Graphic Layer 그래픽 레이어 텍스트 편집 시작 + Begins With 다음으로 시작 + Behind 뒤 + Bessel 베셀 + Bevel Alpha 알파 경사 + Bevel join 경사 연결 + Bezier 베지어 + Bin 빈 + Bin From Selection 선택 항목으로 빈 만들기 + Bin: {name} 빈: {name} + Binary assets added 추가된 바이너리 에셋 + Binary assets removed 삭제된 바이너리 에셋 + Binauralizer - Ambisonics 바이노럴라이저 - Ambisonics + Bins 빈 + Bitrate Encoding 비트 전송률 인코딩 + Bits per Sample: 샘플당 비트: + Black 검정 + Black & White 흑백 + Black and white 흑백 + Black and white with crushed blacks and strong contrast 검정을 뭉갠 강한 대비의 흑백 + Black Input Level 검정 입력 레벨 + Black Video… 블랙 비디오… + Blacks 검정 + Bleach Bypass 블리치 바이패스 + Bleach-bypass style: desaturated with hard contrast 블리치 바이패스 스타일: 저채도와 강한 대비 + Blend 혼합 + Blend Mode 혼합 모드 + Blend With Original 원본과 혼합 + Blending Mode 혼합 모드 + Block Dissolve 블록 디졸브 + Block Height 블록 높이 + Block Motion 블록 모션 + Block Size 블록 크기 + Block Width 블록 너비 + Blocks 블록 + Blue 파랑 + Blue Blurriness 파랑 흐림 + Blue Curve 파랑 곡선 + Blue Offset 파랑 오프셋 + Blue Power 파랑 파워 + Blue Slope 파랑 기울기 + Blue-Blue 파랑-파랑 + Blue-Const 파랑-상수 + Blue-Green 파랑-녹색 + Blue-Red 파랑-빨강 + Blur 흐림 + Blur & Sharpen 흐림 및 선명 + Blur Amount 흐림 양 + Blur Angle 흐림 각도 + Blur Dimensions 흐림 방향 + Blur Dissolve 흐림 디졸브 + Blur In 흐림 인 + Blur Layer 흐림 레이어 + Blur Length 흐림 길이 + Blur Out 흐림 아웃 + Blurriness 흐림 정도 + BMP BMP + BMP Sequence BMP 시퀀스 + Bokeh Blur 보케 흐림 + Boost 부스트 + Boost Vibrance 활기 높이기 + Border 테두리 + Border Color 테두리 색상 + Border Width 테두리 너비 + Borders 테두리 + Bottom 아래쪽 + Bottom Center 하단 중앙 + Bottom Left 왼쪽 하단 + Bottom Right 오른쪽 하단 + Bounce 바운스 + Box 상자 + Box Color 상자 색상 + Box Opacity 상자 불투명도 + Branching 분기 + Breakout to Mono 모노로 분리 + Breakout to mono 모노로 분리 + Bright 밝음 + Bright Air 밝은 공간 + Bright Studio 밝은 스튜디오 + Brightens inside a feathered circular mask 페더 처리된 원형 마스크 안쪽을 밝게 + Brightness 밝기 + Brightness & Contrast 밝기 및 대비 + Bring Forward 앞으로 가져오기 + Bring to Front 맨 앞으로 가져오기 + British Stack 영국식 스택 + Broadcast 방송 + Brown 갈색 + Browse 찾아보기 + Browse files… 파일 찾아보기… + Browse… 찾아보기… + Brush Hardness 브러시 경도 + Brush Opacity 브러시 불투명도 + Brush Position 브러시 위치 + Brush Size 브러시 크기 + Brush Spacing (secs) 브러시 간격(초) + Brush Strokes 브러시 획 + Bubbles 거품 + Bulge 돌출 + Bulge Smoother 돌출 부드럽게 + Bump Height 돌출 높이 + Burn Alpha 알파 번 + Burn Captions Into Video 자막을 비디오에 번인 + Burn Chroma 채도 번 + Burn Color 색상 번 + Butt cap 버트 캡 + Butterworth 버터워스 + Button Editor 버튼 편집기 + Bypass 바이패스 + Cache analysis results for re-use 재사용을 위해 분석 결과 캐시 + Calculate 계산 + Callout – Pointer 콜아웃 – 포인터 + Callout – Tag 콜아웃 – 태그 + Callouts 콜아웃 + Camera 1 카메라 1 + Camera Angle 카메라 앵글 + Camera Angle Metadata 카메라 앵글 메타데이터 + Camera Blur 카메라 흐림 + Camera Names 카메라 이름 + Camera Shake 카메라 흔들림 + Camera {n} — {name} 카메라 {n} — {name} + Cameras shown in the Multi-Camera view (uncheck to hide). 멀티 카메라 보기에 표시할 카메라(숨기려면 선택 해제). + Cancel 취소 + Cancel Dynamic Trim 동적 트림 취소 + Cancel Queued Export 대기 중인 내보내기 취소 + Cancelled 취소됨 + Cannot open {dir}: {e} {dir}을(를) 열 수 없음: {e} + Capitalisation 대소문자 + Caption track options 자막 트랙 옵션 + Caption Tracks 자막 트랙 + Captions 자막 + Captions and Graphics 자막 및 그래픽 + Captions… 자막… + Capture 캡처 + Capture Format: 캡처 형식: + Captured and Generated: 캡처 및 생성: + Captured View: 캡처 화각: + Caribbean 캐리비안 + Case Sensitive 대소문자 구분 + Category: 범주: + Cathedral 대성당 + CBR CBR + CEA-608 CEA-608 + CEA-708 CEA-708 + Cell Pattern 셀 패턴 + Center 중앙 + Center Align Text 텍스트 가운데 정렬 + Center align text 텍스트 가운데 정렬 + Center Channel Pan 센터 채널 팬 + Center Gradient 중앙 그레이디언트 + Center Horizontally 가로 중앙 + Center Input 센터 입력 + Center Left 중앙 왼쪽 + Center of Sphere 구의 중심 + Center Out 센터 출력 + Center Point (Latitude) 중심점(위도) + Center Point (Longitude) 중심점(경도) + Center Right 중앙 오른쪽 + Center Split 중앙 분할 + Center Vertically 세로 중앙 + Centred credits that roll up from below the frame and leave at the top. 프레임 아래에서 올라와 위로 사라지는 가운데 크레딧. + Cents 센트 + Cerulean 세룰리안 + Ch. {n} 채널 {n} + Change Mask 마스크 변경 + Change to Color 색상 변경 + Changes are copied in the background a moment after each edit. If FilmCraft quits unexpectedly, they are offered the next time it starts. 변경 사항은 편집 직후 백그라운드에서 복사됩니다. FilmCraft가 비정상 종료되면 다음 실행 시 복구할 수 있습니다. + Channel 채널 + Channel Blur 채널 흐림 + Channel Format: 채널 형식: + Channel Mix 채널 혼합 + Channel Mixer 채널 믹서 + Channel Volume 채널 볼륨 + Channel {n} 채널 {n} + Channels 채널 + Chaos 혼돈 + Chebyshev 체비셰프 + Checker Wipe 체커 와이프 + Checkerboard 체커보드 + Chinese 중국어 + Choke 초크 + Chorus 코러스 + Chorus/Flanger 코러스/플랜저 + Chroma 채도 + Chroma Leak 채도 번짐 + Chromatic Aberration (Blue) 색수차(파랑) + Chromatic Aberration (Green) 색수차(녹색) + Chromatic Aberration (Red) 색수차(빨강) + Cinematic 시네마틱 + Cineon Converter Cineon 변환기 + Circle 원 + Clamp Signal 신호 제한 + Clarity 명료도 + Clean Combo 클린 콤보 + Clean Up Noisy Dialogue 잡음 섞인 대화 정리 + Clear 지우기 + Clear (Delete) 지우기(삭제) + Clear All 모두 지우기 + Clear All Events 모든 이벤트 지우기 + Clear Audio Type 오디오 유형 지우기 + Clear Edit Point Selection 편집 지점 선택 해제 + Clear Finished 완료 항목 지우기 + Clear Finished Exports 완료된 내보내기 지우기 + Clear Guides 안내선 지우기 + Clear In 시작 표시 지우기 + Clear In and Out 시작/끝 지우기 + Clear Markers 마커 지우기 + Clear Muddiness 탁한 소리 제거 + Clear Out 끝 표시 지우기 + Clear Poster Frame 포스터 프레임 지우기 + Clear Recent Directories 최근 디렉터리 지우기 + Clear Selected Marker 선택한 마커 지우기 + Clear Transition 전환 지우기 + Cleared {n} shortcut(s) of {command} {command}의 단축키 {n}개 지움 + Click a command's Shortcut cell and press keys to assign it; + adds another shortcut. 명령의 단축키 셀을 클릭하고 키를 눌러 할당합니다. +는 단축키를 추가합니다. + Click a key on the keyboard to see its shortcuts 키보드에서 키를 클릭하면 단축키 표시 + Click in the Program monitor to place mask points (drag for curves); click the first point or press Enter to close 프로그램 모니터를 클릭해 마스크 점 배치(드래그 시 곡선), 첫 점 클릭 또는 Enter로 닫기 + Click to select all clips to the left in all tracks. Shift-click for a single track. 클릭하면 모든 트랙에서 왼쪽 클립 모두 선택. Shift-클릭은 단일 트랙. + Click to select all clips to the right in all tracks. Shift-click for a single track. 클릭하면 모든 트랙에서 오른쪽 클립 모두 선택. Shift-클릭은 단일 트랙. + Click to select, or click in empty space and drag to marquee select. Use Shift, Alt, and Ctrl for other options. 클릭하여 선택하거나 빈 곳을 드래그하여 영역 선택. Shift, Alt, Ctrl로 다른 옵션 사용. + Click to select, or click in empty space and drag to marquee select. Use Shift, Opt, and Cmd for other options. 클릭하여 선택하거나 빈 곳을 클릭 후 드래그하여 영역 선택. Shift, Opt, Cmd로 기타 옵션. + Click to split a clip. Shift-click to split all tracks. 클릭하여 클립 자르기. Shift-클릭은 모든 트랙 자르기. + Click to zoom in; Alt-click to zoom out. 클릭하면 확대, Alt-클릭하면 축소. + Click to zoom in; Opt-click to zoom out. 클릭하여 확대. Opt-클릭하여 축소. + Client 클라이언트 + Clip 클립 + Clip Channel Format: 클립 채널 형식: + Clip Fx Editor 클립 Fx 편집기 + Clip ID (fingerprint) 클립 ID(지문) + Clip Keyframes 클립 키프레임 + Clip Level 클립 레벨 + Clip Marker 클립 마커 + Clip Name 클립 이름 + Clip Name: 클립 이름: + Clip Names 클립 이름 + Clip Names and Markers 클립 이름 및 마커 + Clip Overlap: 클립 겹침: + Clip Size 클립 크기 + Clip Speed / Duration 클립 속도/지속 시간 + Clip Volume 클립 볼륨 + Clip {n} 클립 {n} + Clip: {name} 클립: {name} + Clipping 클리핑 + Clips 클립 + Clock Wipe 시계 방향 와이프 + Clock Wipe (Legacy) 시계 방향 와이프(레거시) + Clockwise 시계 방향 + Clone 복제 + Close 닫기 + Close All 모두 닫기 + Close All Other Projects 다른 프로젝트 모두 닫기 + Close All Projects 모든 프로젝트 닫기 + Close Bin 빈 닫기 + Close Gap 간격 닫기 + Close Other Timeline Panels 다른 타임라인 패널 닫기 + Close Panel 패널 닫기 + Close Project 프로젝트 닫기 + Close Sequence 시퀀스 닫기 + Closed Captions 폐쇄 자막 + Codec: 코덱: + Codec: {codec} 코덱: {codec} + Cold Mono 차가운 흑백 + Collect Files and Copy to New Location 파일 수집 후 새 위치로 복사 + Color 색상 + Color & Alpha 색상 및 알파 + Color 1 색상 1 + Color 2 색상 2 + Color 3 색상 3 + Color 4 색상 4 + Color Balance 색상 균형 + Color Burn 색상 번 + Color Channel 색상 채널 + Color Correction 색상 보정 + Color Distortion 색상 왜곡 + Color Dodge 색상 닷지 + Color Emboss 색상 엠보스 + Color Key 색상 키 + Color Management 색상 관리 + Color Management · {n} item 색상 관리 · {n}개 항목 + Color Management · {n} items 색상 관리 · {n}개 항목 + Color Management… 색상 관리… + Color Match 색상 일치 + Color Matte Color 색상 매트 색상 + Color Matte… 색상 매트… + Color Pass 색상 패스 + Color Replace 색상 바꾸기 + Color Space 색 공간 + Color Theme 색상 테마 + Color To Leave 유지할 색상 + Color Tolerance 색상 허용치 + Color Wheels & Match 색상 휠 및 일치 + Color/Black 색상/검정 + Color/Gray 색상/회색 + Color: 색상: + Colorize 색상화 + Color… 색상… + Colour 색상 + Colour Space 색 공간 + Column order 열 순서 + Columns 열 + Command 명령 + Commands: 명령: + Comment 주석 + Comments: 설명: + Commits 커밋 + Community 커뮤니티 + Company 회사 + Comparison 비교 + Comparison View 비교 보기 + Complexity 복잡도 + Composite 합성 + Composite In Back 뒤에 합성 + Composite In Front 앞에 합성 + Composite in Linear Color 선형 색상으로 합성 + Composite on Original 원본에 합성 + Composite on the GPU when available 가능하면 GPU에서 합성 + Composite Using 합성 기준 + Composite Video 합성 비디오 + Compound Blur 복합 흐림 + Compression 압축 + Compression Before Clipping 클리핑 전 압축 + Compression Level 압축 수준 + Compressor 컴프레서 + Configure… 구성… + Conflict: {key} is assigned to {commands} ({ctx}) 충돌: {key}이(가) {commands}에 할당됨({ctx}) + Consolidate and Transcode 통합 및 트랜스코딩 + Consolidate Duplicates 중복 항목 통합 + Constant Gain 일정 게인 + Constant Power 일정 파워 + Containers 컨테이너 + Contains 포함 + Contains: 포함: + Continue 계속 + Continuous 연속 + Continuous Bezier 연속 베지어 + Contrast 대비 + Contributors 기여자 + Conversion Type 변환 유형 + Convert to Master Clip 마스터 클립으로 변환 + Convolution Reverb 컨볼루션 리버브 + Cool Blue 차가운 파랑 + Cool, desaturated and punchy 차갑고 저채도이며 강렬함 + Copy 복사 + Copy and Create Proxies 복사 후 프록시 만들기 + Copy Paste Includes Sequence Markers 복사/붙여넣기 시 시퀀스 마커 포함 + Copyright 저작권 + Core Width 코어 너비 + Corner 모서리 + Corner Pin 모서리 고정 + Corner Radius 모서리 반경 + Correction 보정 + Count 개수 + Countdown Sound Cues 카운트다운 사운드 신호 + Counter-clockwise 시계 반대 방향 + Crash Recovery 충돌 복구 + Crawl Left 왼쪽으로 크롤 + Crawl Right 오른쪽으로 크롤 + Create 4-point polygon mask 4점 다각형 마스크 만들기 + Create a bin of subclips from each detected cut point 감지된 각 컷 지점으로 하위 클립 빈 만들기 + Create a caption track or import a caption file. 자막 트랙을 만들거나 자막 파일을 가져옵니다. + Create a clip marker at each detected cut point 감지된 각 컷 지점에 클립 마커 만들기 + Create captions 자막 만들기 + Create Captions from Transcript… 대본에서 자막 만들기… + Create ellipse mask 타원 마스크 만들기 + Create Folder For Imported Projects 가져온 프로젝트용 폴더 만들기 + Create folder for imported projects 가져온 프로젝트용 폴더 만들기 + Create Multi-Camera Source Sequence 멀티 카메라 소스 시퀀스 만들기 + Create Multi-Camera Source Sequence… 멀티 카메라 소스 시퀀스 만들기… + Create new caption track 새 자막 트랙 만들기 + Create new graphic 새 그래픽 만들기 + Create Proxies 프록시 만들기 + Create Proxies… 프록시 만들기… + Create Search Bin 검색 빈 만들기 + Create Sidecar File 사이드카 파일 만들기 + Creating {n} proxy file(s)… 프록시 파일 {n}개 만드는 중… + Creative 크리에이티브 + Creator 작성자 + Credits 크레딧 + CRF (constant quality) CRF (고정 품질) + Crop 자르기 + Crop Less <-> Smooth More 덜 자르기 <-> 더 부드럽게 + Cross Displacement 교차 변위 + Cross Dissolve 교차 디졸브 + Cross Dissolve (Legacy) 교차 디졸브(레거시) + Cross Processed 크로스 프로세스 + Cross Zoom 교차 확대/축소 + Cross Zoom (Legacy) 교차 확대/축소(레거시) + Crossfade 크로스페이드 + Crossovers 크로스오버 + Crystallize 결정화 + Crystals 결정 + Cube Spin 큐브 회전 + Curl Radius 말림 반경 + Current duration {seconds} s 현재 길이 {seconds} s + Current Time 현재 시간 + Currently: {loc} — {files} file(s), {size} 현재: {loc} — 파일 {files}개, {size} + Cursor 커서 + Curvature 곡률 + Curve 곡선 + Curves 곡선 + Custom 사용자 정의 + Custom ({n} points) 사용자 정의(점 {n}개) + Custom ({ratio}) 사용자 지정 ({ratio}) + Custom Color 사용자 정의 색상 + Custom Setup 사용자 정의 설정 + Cut 잘라내기 + Cut to Camera 1 카메라 1로 전환 + Cut to Camera 2 카메라 2로 전환 + Cut to Camera 3 카메라 3으로 전환 + Cut to Camera 4 카메라 4로 전환 + Cut to Camera 5 카메라 5로 전환 + Cut to Camera 6 카메라 6으로 전환 + Cut to Camera 7 카메라 7로 전환 + Cut to Camera 8 카메라 8로 전환 + Cut to Camera 9 카메라 9로 전환 + Cutoff 차단 + Cyan-teal shadows and warm highlights with a little more contrast 청록 그림자와 따뜻한 하이라이트, 대비 약간 증가 + Cycle (in Revolutions) 주기(회전 수) + Cycle Evolution 주기적 진화 + D1/DV NTSC (0.9091) D1/DV NTSC(0.9091) + D1/DV PAL (1.0940) D1/DV PAL(1.0940) + Damping 감쇠 + Damping HF 고역 감쇠 + Damping LF 저역 감쇠 + Dark 어두움 + Dark Theme 어두운 테마 + Darken 어둡게 하기 + Darkens outside a feathered oval mask 페더 처리된 타원 마스크 바깥을 어둡게 + Darker Color 더 어두운 색상 + Darkest 가장 어둡게 + Date Created 만든 날짜 + Date Modified 수정한 날짜 + Day for night: darker, cooler and less saturated 주간 야경: 더 어둡고 차갑고 저채도 + days 일 + Decay 감쇠 + Decay Time (RT60) 감쇠 시간(RT60) + Decoded frames held for the monitors and thumbnails: {used} of {budget}. 모니터와 썸네일용 디코딩 프레임: {used} / {budget}. + Decoders 디코더 + Decrease Audio Tracks Height 오디오 트랙 높이 줄이기 + Decrease Clip Volume 클립 볼륨 낮추기 + Decrease Clip Volume Many 클립 볼륨 크게 낮추기 + Decrease Font Size by Five Units 글꼴 크기 5단위 줄이기 + Decrease Font Size by One Unit 글꼴 크기 1단위 줄이기 + Decrease Leading by Five Units 행간 5단위 줄이기 + Decrease Leading by One Unit 행간 1단위 줄이기 + Decrease Video Tracks Height 비디오 트랙 높이 줄이기 + DeEss 치찰음 제거 + DeEsser 디에서 + Default 기본값 + Default Audio Tracks 기본 오디오 트랙 + Default clip size: 기본 클립 크기: + Default closed caption font 기본 폐쇄 자막 글꼴 + Default Emoji font 기본 이모지 글꼴 + Default Input 기본 입력 + Default language 기본 언어 + Default location 기본 위치 + Default Media Scaling 기본 미디어 비율 + Default Output 기본 출력 + Default paragraph direction 기본 단락 방향 + Default subtitle and text font 기본 자막 및 텍스트 글꼴 + DeHum 험 제거 + DeHummer 험 제거 + Delay 지연 + Delay 1 지연 1 + Delay 2 지연 2 + Delay 3 지연 3 + Delay 4 지연 4 + Delay and Echo 지연 및 에코 + Delete 삭제 + Delete Arrangement 배열 삭제 + Delete Audio Tracks 오디오 트랙 삭제 + Delete Caption Track 자막 트랙 삭제 + Delete Caption Tracks 자막 트랙 삭제 + Delete Captions 자막 삭제 + Delete Empty Caption Tracks 빈 자막 트랙 삭제 + Delete files older than 다음보다 오래된 파일 삭제 + Delete Graphic Layer 그래픽 레이어 삭제 + Delete Guide Template 안내선 템플릿 삭제 + Delete Layer 레이어 삭제 + Delete Mask 마스크 삭제 + Delete Media Cache Files 미디어 캐시 파일 삭제 + Delete oldest files when the cache exceeds 캐시가 다음을 초과하면 오래된 파일 삭제 + Delete Preset 사전 설정 삭제 + Delete Render Files 렌더 파일 삭제 + Delete Render Files In to Out 시작~끝 렌더 파일 삭제 + Delete Search Bin 검색 빈 삭제 + Delete selected captions 선택한 자막 삭제 + Delete this preset 이 사전 설정 삭제 + Delete Tracks 트랙 삭제 + Delete Tracks… 트랙 삭제… + Delete Transcript 대본 삭제 + Delete Video Tracks 비디오 트랙 삭제 + Deleted preset “{active}” 사전 설정 “{active}” 삭제됨 + Deleted {n} file(s), {size}. 파일 {n}개 삭제됨, {size}. + Delete… 삭제… + Demo Footage 데모 푸티지 + Demo footage 데모 푸티지 + Demo Project 데모 프로젝트 + Demo project 데모 프로젝트 + DeNoise 노이즈 제거 + Denoise 노이즈 제거 + Depth 깊이 + DeReverb 리버브 제거 + Desaturate 채도 감소 + Descending 내림차순 + Description 설명 + Description: 설명: + Deselect All 모두 선택 해제 + Deselect All Project Items 모든 프로젝트 항목 선택 해제 + Destination 대상 위치 + Destination Path 대상 경로 + Destination: 대상 위치: + Destinations 대상 + Detach Proxies 프록시 분리 + Detail Amplitude 세부 진폭 + Detail Level 세부 수준 + Detailed Analysis 상세 분석 + Details: {path} 세부 정보: {path} + Device Class 장치 클래스 + Dialogue 대화 + Diamond 다이아몬드 + Difference 차이 + Dimensions 크기 + Dimmed 흐리게 + Dip to Black 검정으로 물들이기 + Dip to Black (Legacy) 검정으로 물들이기(레거시) + Dip to Color 색상으로 물들이기 + Dip to White 흰색으로 물들이기 + Dip to White (Legacy) 흰색으로 물들이기(레거시) + Direction 방향 + Directional 방향성 + Directional Blur 방향 흐림 + Directional Blur (Legacy) 방향 흐림(레거시) + DirectX 12 only DirectX 12 전용 + Disable video output when in the background 백그라운드에서 비디오 출력 끄기 + Disabled clips 비활성 클립 + Discard 버리기 + Discard take 테이크 삭제 + Discard Unsaved Changes 저장 안 된 변경 사항 취소 + Disk space: original {original} · resulting {result} ({n} files) 디스크 공간: 원본 {original} · 결과 {result}(파일 {n}개) + Disk space: — 디스크 공간: — + Disperse 분산 + Displacement 변위 + Display Color Management 디스플레이 색상 관리 + Display Color Settings 디스플레이 색상 설정 + Display Format: 표시 형식: + Display Mode 표시 모드 + Display name 표시 이름 + Display out of sync indicators for unlinked clips 연결 해제된 클립의 동기화 불일치 표시 + Dissolve 디졸브 + Dissolve Blocks 블록 디졸브 + Distance 거리 + Distance to Image 이미지까지 거리 + Distort 왜곡 + Distortion 왜곡 + Distortion Complexity 왜곡 복잡도 + Distortion Rate 왜곡 속도 + Distortion Type 왜곡 유형 + Distribute 분포 + Distribute Horizontally 가로 분포 + Distribute Space Horizontally 가로 간격 분포 + Distribute Space Vertically 세로 간격 분포 + Distribute Vertically 세로 분포 + Divide 나누기 + Do not delete cache files automatically 캐시 파일 자동 삭제 안 함 + Dock Panel 패널 도킹 + Docking Action 도킹 동작 + Does Not Contain 포함 안 함 + Don't label speakers 화자 레이블 안 함 + Don't Save 저장 안 함 + Done 완료 + Dot Color 점 색상 + Double-click 더블클릭 + Double-click a sequence in the Project panel to open it here. 프로젝트 패널에서 시퀀스를 더블클릭해 여기에 여세요. + Download Speech Model 음성 모델 다운로드 + DPX DPX + DPX Sequence DPX 시퀀스 + Draft decoding at reduced playback resolution (H.264: faster, some frames less filtered) 낮은 재생 해상도에서 드래프트 디코딩(H.264: 빠름, 일부 프레임 필터 감소) + Drag a clip to slide it between its neighbours. 클립을 드래그해 이웃 클립 사이로 슬라이드. + Drag a clip to slip its source in/out without moving it. 클립을 드래그해 이동 없이 소스 시작/끝을 슬립. + Drag an edge to change the clip's speed so it fills the new duration. 가장자리를 드래그해 새 길이에 맞게 클립 속도 변경. + Drag an edit point to ripple trim; later clips move to keep the gap closed. 편집 지점을 드래그해 리플 트림. 뒤 클립이 이동해 간격을 메웁니다. + Drag an edit point to roll it: the out of one clip and the in of the next move together. 편집 지점을 드래그해 롤. 한 클립의 끝과 다음 클립의 시작이 함께 이동합니다. + Drag inward to round the corners 안쪽으로 드래그하여 모서리를 둥글게 + Drag the edge of a music clip to remix it to the new duration at musically matching beats. 음악 클립 가장자리를 드래그해 박자에 맞춰 새 길이로 리믹스. + Drag to scroll the timeline. 드래그하여 타임라인 스크롤. + Drag: move · Alt-click: smooth/corner · Cmd-click: delete 드래그: 이동 · Alt-클릭: 부드럽게/모서리 · Cmd-클릭: 삭제 + Drag: move · Alt-click: smooth/corner · Ctrl-click: delete 드래그: 이동 · Alt-클릭: 부드럽게/모서리 · Ctrl-클릭: 삭제 + Drive 드라이브 + Drop media here to create sequence. 미디어를 여기에 놓아 시퀀스를 만드세요. + Drop Shadow 그림자 효과 + Dry 드라이 + Dry Out 드라이 출력 + Dry Up a Roomy Voice 울리는 목소리 건조하게 + Duck against: 더킹 기준: + Duck Deep Under Dialogue 대화 아래로 깊게 더킹 + Duck Under Dialogue 대화 아래로 더킹 + Duck Under Voice and Effects 음성 및 효과 아래로 더킹 + Ducking 더킹 + Duplicate 복제 + Duration 길이 + Duration: 길이: + Dutch 더치 + DVCPRO HD (1.5) DVCPRO HD(1.5) + Dynamic Audio Waveforms 동적 오디오 파형 + Dynamic Link Dynamic Link + Dynamic Trim Stop 동적 트림 중지 + Dynamics 다이내믹 + Dynamics Processing 다이내믹 처리 + Early Reflections 초기 반사 + Earthquake 지진 + Ease In 천천히 들어가기 + Ease In and Out 천천히 시작 및 끝 + Ease Out 천천히 나오기 + Easing 이징 + Echo 에코 + Echo Glow 에코 광선 + Echo Operator 에코 연산자 + Echo Time (seconds) 에코 시간(초) + Echoes 에코 + Edge Color 가장자리 색상 + Edge Feather 가장자리 페더 + Edge Glow 가장자리 광선 + Edge Sharpness 가장자리 선명도 + Edge Softness 가장자리 부드러움 + Edge Thickness 가장자리 두께 + Edge Thin 가장자리 얇게 + Edge Threshold 가장자리 임계값 + Edge Type 가장자리 유형 + Edge Width 가장자리 너비 + Edit 편집 + Edit Cameras 카메라 편집 + Edit Cameras… 카메라 편집… + Edit Caption Text 자막 텍스트 편집 + Edit Columns 열 편집 + Edit Columns… 열 편집… + Edit Narration 내레이션 편집 + Edit Offline File 오프라인 파일 편집 + Edit Offline… 오프라인 편집… + Edit Original 원본 편집 + Edit Project Notes 프로젝트 메모 편집 + Edit Subclip 하위 클립 편집 + Edit Subclip… 하위 클립 편집… + Edit Workspaces 작업 영역 편집 + Edit Workspaces… 작업 영역 편집… + Editable properties: 편집 가능 속성: + Editing 편집 + Editing Mode: 편집 모드: + Edit… 편집… + EDL… EDL… + Effect 효과 + Effect Controls 효과 컨트롤 + Effect presets 효과 사전 설정 + Effects 효과 + Ellipse 타원 + Ellipse Tool 타원 도구 + Elliptical 타원형 + Embed 포함 + Emboss 엠보스 + Empty tracks 빈 트랙 + Enable 사용 + Enable clip and XMP metadata linking 클립 및 XMP 메타데이터 연결 사용 + Enable developer mode 개발자 모드 사용 + Enable hardware accelerated decoding (requires restart) 하드웨어 가속 디코딩 사용(재시작 필요) + Enable HSL Secondary HSL 보조 사용 + Enable language auto-detection 언어 자동 감지 사용 + Enable Mercury Transmit Mercury Transmit 사용 + Enable ProRes hardware accelerated encoding, if available 가능한 경우 ProRes 하드웨어 가속 인코딩 사용 + Enable proxies 프록시 사용 + Enable Remix 리믹스 사용 + Enable track for recording 트랙 녹음 활성화 + Encapsulate 캡슐화 + Encoder 인코더 + Encoding {n}% 인코딩 {n}% + End Color 끝 색상 + End Credits 엔딩 크레딧 + End Credits – Roll 엔딩 크레딧 – 롤 + End of Ramp 램프 끝 + End Off Screen 화면 밖에서 끝 + End Point 끝점 + End: 끝: + Ends With 다음으로 끝남 + Enhance Speech 음성 향상 + Entire Source 전체 소스 + Enumerate Cameras 카메라 번호 매기기 + EQ EQ + Error 오류 + Errors Only 오류만 + Essential Graphics 기본 그래픽 + Essential Sound 기본 사운드 + EssentialSound EssentialSound + Estimated file size: {size} 예상 파일 크기: {size} + Events 이벤트 + Evolution 진화 + Evolution Options 진화 옵션 + Exact name matches only 정확한 이름만 일치 + Exciter Amount 익사이터 양 + Exciter Mode 익사이터 모드 + Exclude Unused Clips 사용하지 않는 클립 제외 + Exclusion 제외 + Existing XMP files written by other applications are left unchanged. 다른 앱이 쓴 기존 XMP 파일은 변경하지 않습니다. + Exit Trim Mode 트림 모드 종료 + Expand All Tracks 모든 트랙 확장 + Expander 익스팬더 + Exponential Fade 지수 페이드 + Export 내보내기 + Export As Motion Graphics Template 모션 그래픽 템플릿으로 내보내기 + Export As Motion Graphics Template… 모션 그래픽 템플릿으로 내보내기… + Export Audio 오디오 내보내기 + Export formats 내보내기 형식 + Export Frame 프레임 내보내기 + Export Frame (Shift+E) 프레임 내보내기(Shift+E) + Export Frame: use Export mode (M6) 프레임 내보내기: 내보내기 모드 사용(M6) + Export Markers as CSV… 마커를 CSV로 내보내기… + Export Options 내보내기 옵션 + Export presets 내보내기 사전 설정 + Export Preset… 사전 설정 내보내기… + Export the labels to a preset file… 레이블을 사전 설정 파일로 내보내기… + Export: no file chosen (agents: export.presets.export {path, names}) 내보내기: 선택한 파일 없음(에이전트: export.presets.export {path, names}) + Export: no file chosen (agents: shortcuts.export {path}) 내보내기: 선택한 파일 없음(에이전트: shortcuts.export {path}) + Exported graphics template to {path} 그래픽 템플릿을 {path}에 내보냈습니다 + Exported to {path} {path}(으)로 내보냄 + Exported {path} {path} 내보냄 + Exporting 내보내는 중 + Exporting {path} 내보내는 중: {path} + Export… 내보내기… + Exposure 노출 + Extend Next Edit To Playhead 다음 편집을 재생 헤드까지 확장 + Extend Previous Edit To Playhead 이전 편집을 재생 헤드까지 확장 + Extend Selected Edit to Playhead 선택한 편집을 재생 헤드까지 확장 + Extend Selection Down 선택 영역 아래로 확장 + Extend Selection Left 선택 영역 왼쪽으로 확장 + Extend Selection Right 선택 영역 오른쪽으로 확장 + Extend Selection Up 선택 영역 위로 확장 + Extended Dynamic Range Monitoring 확장 다이내믹 레인지 모니터링 + Extract 추출 + Extract (') 추출(') + Extract Audio 오디오 추출 + Extract selected text 선택한 텍스트 추출 + Eyedropper (pick a color from the Program monitor) 스포이드(프로그램 모니터에서 색상 선택) + Eyedropper: no picture to sample 스포이드: 샘플링할 화면 없음 + Face Detection (protect skin tones) 얼굴 감지(피부 톤 보호) + Fade 페이드 + Fade In 페이드 인 + Fade Out 페이드 아웃 + Faded Film 바랜 필름 + Faded Matte 바랜 매트 + Faded Print 바랜 인화 + Fades 페이드 + Failed: {e} 실패: {e} + Falloff 감쇠 + Far Away 먼 거리 + Fast 빠름 + Faster Motion 빠른 모션 + Faux Bold 가짜 굵게 + Faux Italic 가짜 기울임꼴 + Favorites 즐겨찾기 + Favorites only 즐겨찾기만 + FCPXML… FCPXML… + Feather 페더 + Feedback 피드백 + Feedback 1 피드백 1 + Feedback 2 피드백 2 + Feedback 3 피드백 3 + Feedback 4 피드백 4 + Feet + Frames 16mm 피트 + 프레임 16mm + Feet + Frames 35mm 피트 + 프레임 35mm + Female 여성 + Fewer · More 적게 · 많게 + FFT Filter FFT 필터 + Field Options 필드 옵션 + Field Options… 필드 옵션… + Field Order 필드 순서 + Fields: 필드: + File 파일 + File Extension 파일 확장자 + File Format 파일 형식 + File Name 파일 이름 + File Name & Location 파일 이름 및 위치 + File Name: 파일 이름: + File Path 파일 경로 + File Size 파일 크기 + File Types Displayed 표시할 파일 형식 + File… 파일… + Fill 칠 + Fill Background 배경 채우기 + Fill Color 채우기 색상 + Fill frame 프레임 채우기 + Fill Left with Right 왼쪽을 오른쪽으로 채우기 + Fill Point 채우기 지점 + Fill Right with Left 오른쪽을 왼쪽으로 채우기 + Fill Selector 채우기 선택 + Fill the slots above first 위 슬롯을 먼저 채우세요 + Fill Type 채우기 유형 + Film Dissolve 필름 디졸브 + Film Dissolve (Legacy) 필름 디졸브(레거시) + Film Emulation 필름 에뮬레이션 + Film Roll 필름 롤 + FilmCraft always composites in linear light FilmCraft는 항상 선형 광원으로 합성합니다 + FilmCraft Graphics Template FilmCraft 그래픽 템플릿 + FilmCraft Help… FilmCraft 도움말… + FilmCraft hit an error FilmCraft 오류 발생 + FilmCraft keeps the rest of the RAM free for other applications. FilmCraft는 나머지 RAM을 다른 앱용으로 비워 둡니다. + FilmCraft on getartcraft.com getartcraft.com의 FilmCraft + FilmCraft on GitHub GitHub의 FilmCraft + FilmCraft Project FilmCraft 프로젝트 + FilmCraft quit unexpectedly while “{name}” had unsaved changes. “{name}”의 변경 사항을 저장하지 않은 채 FilmCraft가 비정상 종료되었습니다. + FilmCraft was closed while “{name}” had unsaved changes. “{name}”의 변경 사항을 저장하지 않은 채 FilmCraft가 닫혔습니다. + Filter 필터 + Filter and EQ 필터 및 EQ + Filter Frequency 필터 주파수 + Filter Resonance 필터 공진 + Final Cut Pro XML… Final Cut Pro XML… + Final Delay 최종 지연 + Find 찾기 + Find Edges 가장자리 찾기 + Find in: 찾을 위치: + Find Next 다음 찾기 + Find: 찾기: + Find… 찾기… + First commit 첫 커밋 + first marker 첫 마커 + Fit 맞춤 + Fit to frame 프레임에 맞춤 + FLAC FLAC + FLAC (lossless) FLAC(무손실) + FLAC 48 kHz 24-bit FLAC 48 kHz 24비트 + FLAC, stereo 48 kHz 24-bit, lossless FLAC, 스테레오 48 kHz 24비트, 무손실 + Flanger 플랜저 + Flare 플레어 + Flare Brightness 플레어 밝기 + Flare Center 플레어 중심 + Flare Color 플레어 색상 + Flash 플래시 + Flash Cue Marker Flash 큐 마커 + Flatten 병합 + Flicker 깜박임 + Flicker Removal 깜박임 제거 + Flickers 깜박임 + Flip Motion 뒤집기 모션 + Flip Over 뒤집기 + Float 부동 + Focus 초점 + Focus Blur 초점 흐림 + Focus Point 초점 + Focus Shape 초점 모양 + Focus Size 초점 크기 + Fold Motion 접기 모션 + Foldback 폴드백 + Folder 폴더 + Font 글꼴 + Font family 글꼴 패밀리 + Font Size 글꼴 크기 + Font Style 글꼴 스타일 + Fonts used in the project: 프로젝트에 사용된 글꼴: + For color management parameters, go to the Settings tab in the Lumetri Color panel. 색상 관리 매개변수는 Lumetri 색상 패널의 설정 탭에서 지정합니다. + Forest 포레스트 + Format 형식 + Format and size 형식 및 크기 + Four Scopes (RGB, 8 bit) 4개 스코프(RGB, 8비트) + Four Scopes (YUV, float, no clamp) 4개 스코프(YUV, 부동, 클램프 없음) + Fractal Influence 프랙탈 영향 + Fractal Type 프랙탈 유형 + Frame 프레임 + Frame Blending 프레임 혼합 + Frame cache for monitors and thumbnails 모니터 및 썸네일용 프레임 캐시 + Frame Color 프레임 색상 + Frame Count 프레임 수 + Frame Hold Options 프레임 고정 옵션 + Frame Hold Options… 프레임 고정 옵션… + Frame Layout 프레임 레이아웃 + Frame Rate 프레임 속도 + Frame Rate: 프레임 속도: + Frame Sampling 프레임 샘플링 + Frame Size 프레임 크기 + Frame Size / Rate 프레임 크기/속도 + Frame Size: 프레임 크기: + Frame Width 프레임 너비 + Frames 프레임 + frames 프레임 + Frames per Still: 스틸당 프레임: + Framing 프레이밍 + Free draw bezier 자유 그리기 베지어 + Freeform 자유형 + Freeform View 자유형 보기 + Freeform View Options 자유형 보기 옵션 + Freeform View Options… 자유형 보기 옵션… + French 프랑스어 + Frequency 주파수 + From 시작 + From Bin 빈에서 + From East 동쪽에서 + From file… 파일에서… + From North 북쪽에서 + From Source Monitor 소스 모니터에서 + From Source Monitor, Match Frame 소스 모니터에서, 프레임 일치 + From South 남쪽에서 + From the Left 왼쪽에서 + From the Right 오른쪽에서 + From West 서쪽에서 + Front + LFE 전면 + LFE + Front + Rear 전면 + 후면 + Front + Rear + LFE 전면 + 후면 + LFE + Front Only 전면만 + Full 전체 + Full screen 전체 화면 + Fuzz 퍼즈 + Gain 게인 + Gamma 감마 + Gamma Correction 감마 보정 + Gamut Warning 색역 경고 + Gamut Warning Color 색역 경고 색상 + Gang to Program 프로그램에 연동 + Gap 간격 + Gap Color 간격 색상 + Gaps 간격 + Gaussian Blur 가우시안 흐림 + Gaussian Blur (Legacy) 가우시안 흐림(레거시) + Gaussian Blur from 0 to 60 over the last second 마지막 1초 동안 가우시안 흐림 0→60 + Gaussian Blur from 60 to 0 over the first second 처음 1초 동안 가우시안 흐림 60→0 + GB GB + General 일반 + General… 일반… + Generate 생성 + Generate Audio Waveform 오디오 파형 생성 + Generate Ducking Keyframes 더킹 키프레임 생성 + Generate Keyframes 키프레임 생성 + Generate Timecode 타임코드 생성 + Generate waveforms and conform audio using multithreading 멀티스레딩으로 파형 생성 및 오디오 컨폼 + Generate waveforms automatically during import 가져오기 중 파형 자동 생성 + Generating waveform… 파형 생성 중… + German 독일어 + Get Media File Properties for 미디어 파일 속성 가져오기: + Get Project Notes 프로젝트 메모 가져오기 + Ghosts 고스트 + Glass 유리 + Glint 반짝임 + Glitch 글리치 + Glow 광선 + Glow Brightness 광선 밝기 + Glow Color 광선 색상 + Glow Only 광선만 + Glow Radius 광선 반경 + Glow Saturation 광선 채도 + Glow Width 광선 너비 + Go Back 뒤로 + Go Forward 앞으로 + Go to Gap 간격으로 이동 + Go to In 시작 표시로 이동 + Go to In (Shift+I) 시작으로 이동(Shift+I) + Go to In Point 시작 표시(In)로 이동 + Go to Next Caption Segment 다음 자막 세그먼트로 이동 + Go to Next Edit Point 다음 편집 지점으로 이동 + Go to Next Edit Point on Any Track 모든 트랙의 다음 편집 지점으로 이동 + Go to next keyframe 다음 키프레임으로 이동 + Go to Next Marker 다음 마커로 이동 + Go to Out 끝 표시로 이동 + Go to Out (Shift+O) 끝으로 이동(Shift+O) + Go to Out Point 끝 표시(Out)로 이동 + Go to Previous Caption Segment 이전 자막 세그먼트로 이동 + Go to Previous Edit Point 이전 편집 지점으로 이동 + Go to Previous Edit Point on Any Track 모든 트랙의 이전 편집 지점으로 이동 + Go to previous keyframe 이전 키프레임으로 이동 + Go to Previous Marker 이전 마커로 이동 + Go to Selected Clip End 선택한 클립 끝으로 이동 + Go to Selected Clip Start 선택한 클립 시작으로 이동 + Go to Sequence End 시퀀스 끝으로 이동 + Go to Sequence Start 시퀀스 시작으로 이동 + Go to Split 분할 지점으로 이동 + Golden Dusk 황금빛 황혼 + Golden Hour 골든 아워 + Good 좋음 + GPU acceleration is unavailable; preview renders on the CPU GPU 가속 사용 불가. 미리 보기는 CPU로 렌더 + GPU Rendering GPU 렌더링 + Grab bag 모음 + Gradient 그레이디언트 + Gradient Angle 그레이디언트 각도 + Gradient End 그레이디언트 끝 + Gradient Layer 그레이디언트 레이어 + Gradient Placement 그레이디언트 배치 + Gradient Scatter 그레이디언트 분산 + Gradient Shape 그레이디언트 모양 + Gradient Start 그레이디언트 시작 + Gradient Wipe 그레이디언트 와이프 + Gradient Wipe (Legacy) 그레이디언트 와이프(레거시) + Grain Size 그레인 크기 + Graphic Equalizer (10 Bands) 그래픽 이퀄라이저(10밴드) + Graphic Equalizer (20 Bands) 그래픽 이퀄라이저(20밴드) + Graphic Equalizer (30 Bands) 그래픽 이퀄라이저(30밴드) + Graphics 그래픽 + Graphics and Titles 그래픽 및 제목 + Graphics problem: switched to software compositing 그래픽 문제: 소프트웨어 합성으로 전환됨 + Graphics text search arrives with M10.1–M10.2 그래픽 텍스트 검색은 M10.1–M10.2에서 제공 + Graphics… 그래픽… + Green 초록 + Green Blurriness 녹색 흐림 + Green Curve 녹색 커브 + Green Offset 녹색 오프셋 + Green Power 녹색 파워 + Green Slope 녹색 기울기 + Green-Blue 녹색-파랑 + Green-Const 녹색-상수 + Green-Green 녹색-녹색 + Green-Red 녹색-빨강 + Grid 격자 + Grid spacing: 격자 간격: + Group 그룹 + Grow 확대 + Growing Files 증가하는 파일 + Grunge 그런지 + Grunge & Distort 그런지 및 왜곡 + Guide Templates 안내선 템플릿 + Guides 안내선 + GuitarSuite GuitarSuite + H.264 H.264 + H.264 1920×1080 for video sharing sites, VBR 1 pass, 16 Mbps H.264 1920×1080 동영상 공유 사이트용, VBR 1패스, 16 Mbps + H.264 1920×1080, VBR 1 pass, 20 Mbps target, AAC 320 kbps H.264 1920×1080, VBR 1패스, 목표 20 Mbps, AAC 320 kbps + H.264 1920×1080, VBR 2 pass, 18 Mbps H.264 1920×1080, VBR 2패스, 18 Mbps + H.264 3840×2160 for video sharing sites, VBR 1 pass, 45 Mbps H.264 3840×2160 동영상 공유 사이트용, VBR 1패스, 45 Mbps + H.264 3840×2160, VBR 1 pass, 60 Mbps target, AAC 320 kbps H.264 3840×2160, VBR 1패스, 목표 60 Mbps, AAC 320 kbps + H.264 3840×2160, VBR 2 pass, 50 Mbps H.264 3840×2160, VBR 2패스, 50 Mbps + H.264 9:16 portrait for short-form social video, scaled to fill, 12 Mbps H.264 9:16 세로, 숏폼 소셜 비디오용, 채우기 비율, 12 Mbps + H.264 at the sequence's frame size and rate; 0.05 bits per pixel H.264, 시퀀스 프레임 크기·속도, 픽셀당 0.05비트 + H.264 at the sequence's frame size and rate; 0.1 bits per pixel H.264, 시퀀스 프레임 크기·속도, 픽셀당 0.1비트 + H.264 at the sequence's frame size and rate; bitrate adapts to the frame size (0.2 bits per pixel) H.264, 시퀀스 프레임 크기·속도, 비트 전송률은 프레임 크기에 맞춤(픽셀당 0.2비트) + H.265 (HEVC) H.265 (HEVC) + Half Saturation 채도 절반 + Hand Tool 손 도구 + Handle Frames: 핸들 프레임: + Hard 하드 + Hard Clip 하드 클립 + Hard Light 하드 라이트 + Hard Limiter 하드 리미터 + Hard Mix 하드 혼합 + Hard Mono 강한 흑백 + Hard Sun 강한 햇빛 + Hardware (AV1 has no software encoder) 하드웨어 (AV1은 소프트웨어 인코더 없음) + Hardware (H.265 has no software encoder) 하드웨어 (H.265는 소프트웨어 인코더 없음) + Hardware decoding 하드웨어 디코딩 + Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API or NVDEC for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. 하드웨어 디코딩: 자동은 지원 스트림에 시스템 비디오 디코더(macOS는 VideoToolbox, Windows는 Media Foundation, Linux는 H.264·HEVC용 VA-API 또는 NVDEC)를 쓰고, 그 외나 하드웨어 실패 시 FilmCraft 자체 디코더를 씁니다. 이미 열린 미디어는 다시 열 때까지 기존 디코더를 유지합니다. + HD Anamorphic 1080 (1.333) HD 아나모픽 1080(1.333) + HDR HDR + HDR Graphics White (Nits) HDR 그래픽 흰색(Nits) + HDR Range HDR 범위 + HDR Specular HDR 반사광 + HDR White HDR 흰색 + Head 헤드 + Head and Tail 헤드와 테일 + Head Size 머리 크기 + Heading 머리글 + Heart 하트 + Height 높이 + Height: 높이: + Help 도움말 + Hexagon 육각형 + Hide All Caption Tracks 모든 자막 트랙 숨기기 + Hide Back Face 뒷면 숨기기 + High 높음 + High contrast with held highlights and deep shadows 밝은 영역은 유지하고 그림자는 깊은 고대비 + High Cutoff 고역 차단 + High Frequency Cut 고주파 차단 + High Frequency Damping 고주파 감쇠 + High Pass 하이 패스 + High Quality 1080p HD 고품질 1080p HD + High Quality 2160p 4K 고품질 2160p 4K + High Quality Playback 고품질 재생 + High Shelf 하이 셸프 + High Shelf Gain 하이 셸프 게인 + High Tone 고음 + Highlight 밝은 영역 + Highlight Blue Balance 밝은 영역 파랑 균형 + Highlight Brightness 밝은 영역 명도 + Highlight Color 밝은 영역 색상 + Highlight Green Balance 밝은 영역 녹색 균형 + Highlight Red Balance 밝은 영역 빨강 균형 + Highlight Rolloff 밝은 영역 롤오프 + Highlight Threshold 밝은 영역 임계값 + Highlight Tint 밝은 영역 색조 + Highlights 밝은 영역 + Highlights Lightness 밝은 영역 밝기 + Highpass 하이패스 + Hindi 힌디어 + Hindi digits 힌디어 숫자 + Histogram 히스토그램 + History 작업 내역 + HLS Vectorscope + Histogram HLS 벡터스코프 + 히스토그램 + Hold 홀드 + Hold Filters 필터 고정 + Hold On 고정 위치 + Home 홈 + Horizontal 가로 + horizontal 가로 + Horizontal 16:9 가로 16:9 + Horizontal Alignment 가로 정렬 + Horizontal and Vertical 가로 및 세로 + Horizontal Blocks 가로 블록 + Horizontal Decentering 가로 편심 + Horizontal Displacement 가로 변위 + Horizontal Flip 가로 뒤집기 + Horizontal Margin 가로 여백 + Hover Scrub 마우스오버 스크럽 + HSL Secondary HSL 보조 + Hue 색조 + Hue Center 색조 중심 + Hue Range 색조 범위 + Hue Saturation Curves 색조 채도 곡선 + Hue Shift 색조 이동 + Hue Tolerance 색조 허용치 + Hue vs Hue 색조 대 색조 + Hue vs Luma 색조 대 휘도 + Hue vs Sat 색조 대 채도 + I/O Buffer Size I/O 버퍼 크기 + Icon 아이콘 + Icon View 아이콘 보기 + Ignore Alpha 알파 무시 + Ignore Audio 오디오 무시 + Ignore Hours 시간 무시 + Ignore Options 무시 옵션 + Ignore Video 비디오 무시 + Image 이미지 + Image Control 이미지 컨트롤 + Image Overlay 이미지 오버레이 + Image Sequence 이미지 시퀀스 + Immersive Video 몰입형 비디오 + Import 가져오기 + Import a preset file… 사전 설정 파일 가져오기… + Import as Image Sequence 이미지 시퀀스로 가져오기 + Import captions file… 자막 파일 가져오기… + Import From 가져오기 위치 + Import from Media Browser 미디어 브라우저에서 가져오기 + Import Image Sequence… 이미지 시퀀스 가져오기… + Import numbered stills as image sequences 번호가 매겨진 스틸을 이미지 시퀀스로 가져오기 + Import Presets… 사전 설정 가져오기… + Import: no file chosen (agents: export.presets.import {path}) 가져오기: 선택한 파일 없음(에이전트: export.presets.import {path}) + Import: no file chosen (agents: shortcuts.import {path}) 가져오기: 선택한 파일 없음(에이전트: shortcuts.import {path}) + Imported “{name}” “{name}” 가져옴 + Import… 가져오기… + Impulse 임펄스 + In 시작 + in 시작 + In a Room 방 안 + In Point 시작 표시(In) + In Points 시작 표시 + In the Media Cache 미디어 캐시에 + In the project 프로젝트에 + In/Out Duration 시작/끝 지속 시간 + In: {name} 시작: {name} + Include Alpha Channel 알파 채널 포함 + Include captions on import 가져올 때 자막 포함 + Include Handles: 핸들 포함: + Include Preview Files 미리보기 파일 포함 + Include Proxies 프록시 포함 + Incoming 들어오는 + Increase Audio Tracks Height 오디오 트랙 높이 늘리기 + Increase Clip Volume 클립 볼륨 높이기 + Increase Clip Volume Many 클립 볼륨 크게 높이기 + Increase Font Size by Five Units 글꼴 크기 5단위 늘리기 + Increase Font Size by One Unit 글꼴 크기 1단위 늘리기 + Increase Leading by Five Units 행간 5단위 늘리기 + Increase Leading by One Unit 행간 1단위 늘리기 + Increase Video Tracks Height 비디오 트랙 높이 늘리기 + Indeterminate Media Timebase 불확정 미디어 타임베이스 + Info 정보 + Ingest 수집 + Ingest Settings 수집 설정 + Ingest Settings… 수집 설정… + Initial Delay 초기 지연 + Inner 안쪽 + Inner Radius 안쪽 반경 + Input Boost 입력 부스트 + Input LUT 입력 LUT + Input: 입력: + Insert 삽입 + Insert (,) 삽입(,) + Insert and overwrite sequences as nests or individual clips 시퀀스를 중첩 또는 개별 클립으로 삽입 및 덮어쓰기 + Insert Edit 삽입 편집 + Insert Frame Hold Segment 프레임 고정 세그먼트 삽입 + Inset 인셋 + Inside 내부 + Inside Color 내부 색상 + Install Motion Graphics Template (.fcgt) 모션 그래픽 템플릿 설치(.fcgt) + Install Motion Graphics Template… 모션 그래픽 템플릿 설치… + Install… 설치… + Intensity 강도 + Inter Inter + Interface Language 인터페이스 언어 + Interior Room Tone 실내 룸톤 + Internal Black Point 내부 검정 점 + Internal White Point 내부 흰색 점 + Interpolation 보간 + Interpret Footage 푸티지 해석 + Interpret Footage… 푸티지 해석… + Intersect 교차 + Intro Duration 인트로 길이 + Inverse Falloff 역 감쇠 + Invert 반전 + Invert Alpha 알파 반전 + Invert Blur 흐림 반전 + Invert Fill 채우기 반전 + Invert Gradient 그레이디언트 반전 + Invert Left 왼쪽 반전 + Invert Right 오른쪽 반전 + Inverted 반전됨 + Iris 아이리스 + Iris Box 아이리스 상자 + Iris Cross 아이리스 십자 + Iris Diamond 아이리스 다이아몬드 + Iris Radius 아이리스 반경 + Iris Rotation 아이리스 회전 + Iris Round 아이리스 원형 + Iris Shape 아이리스 모양 + Italian 이탈리아어 + Japanese 일본어 + JetBrains Mono JetBrains Mono + Join the ArtCraft Discord ArtCraft Discord 참여 + Join the ArtCraft Discord (discord.gg/artcraft) ArtCraft Discord 참여(discord.gg/artcraft) + Join the ArtCraft Discord… ArtCraft Discord 참여… + Join Through Edits 통과 편집 결합 + Join us on Discord Discord 참여하기 + JPEG JPEG + JPEG Sequence JPEG 시퀀스 + just now 방금 + Justify 양쪽 정렬 + Kaleidoscope 만화경 + Keep a recovery copy of unsaved changes 저장 안 된 변경 사항의 복구본 유지 + Keep: 유지: + Kerning 커닝 + Key 키 + Key Color 키 색상 + Key Frame Distance 키프레임 간격 + Key: 키: + Keyboard Shortcuts 키보드 단축키 + Keyboard Shortcuts… 키보드 단축키… + Keyboard: US 키보드: 미국 + Keyboard: US (macOS) 키보드: 미국(macOS) + Keying 키잉 + Korean 한국어 + Label 레이블 + Label Color Presets 레이블 색상 사전 설정 + Label Colors 레이블 색상 + Label Defaults 레이블 기본값 + Label speakers 화자 레이블 지정 + Labels 레이블 + Labels… 레이블… + Language 언어 + Language: 언어: + Large blocks resolving to the picture over the first second 처음 1초 동안 큰 블록이 화면으로 선명해짐 + Large centred title, accent rule and subtitle. 큰 가운데 제목, 강조선, 부제목. + Large Hall 대형 홀 + Large Trim Offset 큰 트림 오프셋 + Large Volume Adjustment 큰 볼륨 조정 + Larger 크게 + Last auto-save {a} 마지막 자동 저장 {a} + Last auto-save {a} · last recovery copy {j} 마지막 자동 저장 {a} · 마지막 복구 사본 {j} + Last commit 마지막 커밋 + Last recovery copy {j} 마지막 복구 사본 {j} + Latch 래치 + Lavender 라벤더 + Layer Name 레이어 이름 + Layers 레이어 + Layout: 레이아웃: + Leading 행간 + Learning 학습 + Leave Color 색상 유지 + Left 왼쪽 + Left - Left 왼쪽 - 왼쪽 + Left - Right 왼쪽 - 오른쪽 + Left Align Text 텍스트 왼쪽 정렬 + Left align text 텍스트 왼쪽 정렬 + Left channel to device output 왼쪽 채널을 장치 출력으로 + Left to Right 왼쪽에서 오른쪽 + Left to right 왼쪽에서 오른쪽 + Legacy 레거시 + Legal Range Squeeze 허용 범위로 압축 + Length 길이 + Lens Blur 렌즈 흐림 + Lens Distortion 렌즈 왜곡 + Lens Flare 렌즈 플레어 + Lens Type 렌즈 유형 + Level 레벨 + Level 1 레벨 1 + Level 2 레벨 2 + Level 3 레벨 3 + Level 4 레벨 4 + Levels 레벨 + Libraries 라이브러리 + Lift 들어올리기 + Lift (;) 들어내기(;) + Lift selected text 선택한 텍스트 들어내기 + Lift Shadows 섀도 올리기 + Lifted blacks, softer contrast, muted colour 들뜬 검정, 부드러운 대비, 차분한 색상 + Ligatures 합자 + Light 밝게 + Light 1 조명 1 + Light 2 조명 2 + Light 3 조명 3 + Light 4 조명 4 + Light 5 조명 5 + Light Angle 조명 각도 + Light Color 조명 색상 + Light Intensity 조명 강도 + Light Leak 라이트 릭 + Light Leaks 라이트 릭 + Light Sweep 라이트 스윕 + Light Theme 밝은 테마 + Light Type 조명 유형 + Lighten 밝게 하기 + Lighter Color 밝은 색상 + Lighting Effects 조명 효과 + Lightning 번개 + Lights & Blurs 조명 및 흐림 + Lights & Glows 조명 및 글로우 + Limiter 리미터 + Limiter Release 리미터 릴리스 + Limiter Threshold 리미터 임계값 + Line delta 줄 변화량 + Line Thickness 선 두께 + Linear 선형 + Linear Burn 선형 번 + Linear Dodge (Add) 선형 닷지(추가) + Linear Gradient 선형 그레이디언트 + Linear keyframe thinning 선형 키프레임 줄이기 + Linear Light 선형 라이트 + Linear Ramp 선형 램프 + Linear to Log 선형에서 로그로 + Linear Wipe 선형 닦아내기 + Linear Wipe (Legacy) 선형 닦아내기(레거시) + Lines +/− 줄 +/− + Lines added 추가된 줄 + Lines deleted 삭제된 줄 + Link 연결 + Link Channels 채널 연결 + Link Media 미디어 연결 + Link Media… 미디어 연결… + Link to Media 미디어에 연결 + Linked Selection 연결된 선택 + Linked {n} clip(s) 클립 {n}개 연결됨 + Liquid Distortion 액체 왜곡 + List 목록 + List View 목록 보기 + Loaded “{name}” “{name}” 불러옴 + Local Drives 로컬 드라이브 + Local Projects 로컬 프로젝트 + Locate… 찾기… + Location 위치 + Lock Guides 안내선 잠금 + Log Note 로그 메모 + Log Note: 로그 메모: + Log to Linear 로그에서 선형으로 + Log to Log 로그에서 로그로 + Logo Cutout 로고 잘라내기 + Long Shadow 긴 그림자 + Look 룩 + Look LUT 룩 LUT + Look-Ahead Time 예측 시간 + Loop 반복 + Loop marked range 표시 범위 반복 + Looping GIF, 640×360 at 15 fps 반복 GIF, 640×360, 15 fps + Loudness 라우드니스 + Loudness Maximizer 라우드니스 최대화 + Loudness Meter 라우드니스 미터 + Loudness Normalization 라우드니스 정규화 + Louver 루버 + Low 낮음 + Low Frequency Cut 저주파 차단 + Low Pass 로우 패스 + Low Shelf 로우 셸프 + Low Shelf Gain 로우 셸프 게인 + Low Tone 저음 + Lower First 아래쪽 필드 우선 + Lower Left 왼쪽 아래 + Lower Right 오른쪽 아래 + Lower Third – Rule 하단 자막 – 선 + Lower Third – Slab 하단 자막 – 판 + Lower Thirds 하단 자막 + lowest (V1…) 가장 낮음(V1…) + Lowpass 로우패스 + Luma 루마 + Luma and Chroma 루마 및 크로마 + Luma Curve 루마 커브 + Luma Fade 루마 페이드 + Luma Key 루마 키 + Luma Max 휘도 최대 + Luma Min 휘도 최소 + Luma Min / Max 루마 최소/최대 + Luma Source 루마 소스 + Luma vs Sat 휘도 대 채도 + Lumetri Lumetri + Lumetri Color Lumetri 색상 + Lumetri Presets Lumetri 사전 설정 + Lumetri Scopes Lumetri 스코프 + Luminance 휘도 + Luminance Threshold 휘도 임계값 + Luminosity 광도 + LUT LUT + Magenta 마젠타 + Magnification 확대/축소 비율 + Magnify 돋보기 + Magnify (Legacy) 돋보기(레거시) + Main Main + Main (8-bit) Main (8비트) + Main (8-bit; Main 10 for HDR sequences) Main (8비트, HDR 시퀀스는 Main 10) + Main 10 (10-bit, HDR) Main 10 (10비트, HDR) + Maintain pitch while shuttling 셔틀 시 피치 유지 + Major Radius 장반경 + Make Offline 오프라인으로 만들기 + Make Offline… 오프라인으로 만들기… + Make Subclip 하위 클립 만들기 + Make Subclip… 하위 클립 만들기… + Make Subsequence 하위 시퀀스 만들기 + Make {n} selected clip(s) offline. Their clips show the offline slate until you link them again. 선택한 클립 {n}개를 오프라인으로 만듭니다. 다시 연결할 때까지 오프라인 슬레이트가 표시됩니다. + Makeup 메이크업 + Male 남성 + Manage Guides 안내선 관리 + Manage Guides… 안내선 관리… + Manage Saved View Presets 저장된 보기 사전 설정 관리 + Mango 망고 + Map Black To 검정 매핑 대상 + Map White To 흰색 매핑 대상 + Maps full-range levels into 16–235 (video range) with a luma curve 전체 범위 레벨을 루마 커브로 16–235(비디오 범위)에 매핑 + Mark Clip 클립 표시 + Mark In 시작 표시 + Mark In (Hover Scrub) 시작 표시(호버 스크럽) + Mark In (I) 시작 표시(I) + Mark Out 끝 표시 + Mark Out (Hover Scrub) 끝 표시(호버 스크럽) + Mark Out (O) 끝 표시(O) + Mark Selection 선택 영역 표시 + Mark Split 분할 표시 + Marker 마커 + Markers 마커 + Mask Only 마스크만 + Masks 마스크 + Master Amplitude 마스터 진폭 + Master Gain 마스터 게인 + Master volume: use the Audio Track Mixer 마스터 볼륨: 오디오 트랙 믹서 사용 + Master · {name} 마스터 · {name} + Mastering 마스터링 + Match file properties: 일치시킬 파일 속성: + Match Frame 프레임 일치 + Match frame sets in point 프레임 일치 시 시작 표시 설정 + Match Playhead 재생 헤드 일치 + Match Source 소스와 일치 + Match Source ({seq_sr} Hz) 소스와 일치({seq_sr} Hz) + Match Source – Adaptive High Bitrate 소스와 일치 – 적응형 높은 비트 전송률 + Match Source – Adaptive Low Bitrate 소스와 일치 – 적응형 낮은 비트 전송률 + Match Source – Adaptive Medium Bitrate 소스와 일치 – 적응형 중간 비트 전송률 + Match {n} of {total} {total}개 중 {n}번째 일치 + Match: 일치: + Matches 일치 + Matches each of the {n} clips on its own. 클립 {n}개를 각각 개별 일치시킵니다. + Matte 매트 + Matte Alpha 매트 알파 + Matte Cleanup 매트 정리 + Matte Generation 매트 생성 + Matte Luma 매트 루마 + Maximize Frame 프레임 최대화 + Maximize or Restore Active Frame 활성 프레임 최대화/복원 + Maximize or Restore Frame Under Cursor 커서 아래 프레임 최대화/복원 + Maximum 최대 + Maximum Amplitude 최대 진폭 + Maximum Bit Depth 최대 비트 심도 + Maximum Bitrate 최대 비트 전송률 + Maximum Block Size 최대 블록 크기 + Maximum Blur 최대 흐림 + Maximum Project Versions 최대 프로젝트 버전 수 + Maximum Reduction 최대 감소량 + Maximum Render Quality 최대 렌더 품질 + Maximum Scale 최대 비율 + MB MB + Media 미디어 + Media Analysis 미디어 분석 + Media Analysis & Transcription 미디어 분석 및 전사 + Media Analysis & Transcription… 미디어 분석 및 받아쓰기… + Media analysis and transcriptions will be cached in the Media Cache folder. Reanalysis won't be required if the media is imported into another project on this computer. 미디어 분석과 전사는 미디어 캐시 폴더에 캐시됩니다. 이 컴퓨터의 다른 프로젝트로 가져와도 재분석이 필요 없습니다. + Media Browser 미디어 브라우저 + Media Browser Settings 미디어 브라우저 설정 + Media Cache 미디어 캐시 + Media Cache Database 미디어 캐시 데이터베이스 + Media Cache Files 미디어 캐시 파일 + Media Cache Management 미디어 캐시 관리 + Media Cache… 미디어 캐시… + Media colour space (from file metadata): {detected} 미디어 색 공간(파일 메타데이터): {detected} + Media Duration 미디어 지속 시간 + Media End 미디어 끝 + Media File 미디어 파일 + Media files are deleted 미디어 파일 삭제 + Media files remain on disk 미디어 파일을 디스크에 유지 + Media missing 미디어 누락 + Media Name: 미디어 이름: + Media Source Channels 미디어 소스 채널 + Media Start 미디어 시작 + Media Start: 미디어 시작: + Media Time 미디어 시간 + Media Timecode 미디어 타임코드 + Media Type 미디어 유형 + MediaBrowser 미디어 브라우저 + MediaCache 미디어 캐시 + Median 중앙값 + Media… 미디어… + Medium 중간 + Medium Room 중형 룸 + Memory 메모리 + Memory… 메모리… + Merge Captions 자막 병합 + Merge Clips 클립 병합 + Merge Clips… 클립 병합… + Merge selected captions 선택한 자막 병합 + Merged clip created 병합 클립 생성됨 + Merged PRs 병합된 PR + Metadata 메타데이터 + Metadata & Timecode Burn-in 메타데이터 및 타임코드 번인 + Metadata Display 메타데이터 표시 + Metadata Display… 메타데이터 표시… + Meter Input(s) Only 입력만 미터 표시 + Method 방법 + Method: 방법: + Metrics Kerning 메트릭 커닝 + Mid 중간 + Mid Point 중간점 + Midpoint 중간점 + Midtone Blue Balance 중간 톤 파랑 균형 + Midtone Green Balance 중간 톤 녹색 균형 + Midtone Red Balance 중간 톤 빨강 균형 + Midtones 중간 톤 + Midtones Lightness 중간 톤 밝기 + Milliseconds 밀리초 + milliseconds 밀리초 + Minimize All Tracks 모든 트랙 최소화 + Minimum 최소 + Minimum time 최소 시간 + Minimum time interval thinning 최소 시간 간격 줄이기 + Minor Radius 단반경 + minute(s) 분 + Mirror 미러 + Mirror Alternate 교대 미러 + Missing 없음 + Missing font replacement 누락된 글꼴 대체 + MIT OR Apache-2.0. Fonts: Inter, Noto Serif and JetBrains Mono (SIL OFL 1.1). MIT 또는 Apache-2.0. 글꼴: Inter, Noto Serif, JetBrains Mono(SIL OFL 1.1). + Miter join 마이터 연결 + Miter limit 마이터 한계 + Mix 믹스 + Mixdown video 비디오 믹스다운 + Mixed Crystals 혼합 결정 + Mixer 믹서 + Mode 모드 + Model 모델 + Models 모델 + Modern High Gain 모던 하이 게인 + Modifier held / selected 보조키 누름/선택됨 + Modifiers 보조키 + Modify 수정 + Modify Clip: Audio Channels 클립 수정: 오디오 채널 + Modify Clip: Timecode 클립 수정: 타임코드 + Modulation 변조 + Modulation Rate 변조 속도 + Mono 모노 + Mono Media 모노 미디어 + Monochrome 단색 + Monochrome toned cyan-blue 청록색 톤 단색 + Monochrome toned warm brown 따뜻한 갈색 톤 단색 + Monoscopic 모노스코픽 + Monospace only 고정폭만 + More colour in the less saturated areas 채도 낮은 영역에 색상 추가 + More presets… 사전 설정 더 보기… + More transport controls 전송 컨트롤 더 보기 + Morph Cut 모프 컷 + Mosaic 모자이크 + Mosaic (Legacy) 모자이크(레거시) + Mosaic In 모자이크 인 + Motion 모션 + Motion Blur 모션 흐림 + Motion Camera 모션 카메라 + Motion Graphics Template… 모션 그래픽 템플릿… + Motion Preset 모션 사전 설정 + Motion Tween 모션 트윈 + Move 이동 + Move All Audio Targets Down 모든 오디오 대상 아래로 이동 + Move All Audio Targets Up 모든 오디오 대상 위로 이동 + Move All Video Targets Down 모든 비디오 대상 아래로 이동 + Move All Video Targets Up 모든 비디오 대상 위로 이동 + Move clips to the lowest track possible 클립을 가능한 가장 낮은 트랙으로 이동 + Move Selection Down 선택 영역 아래로 이동 + Move Selection End 선택 영역 끝으로 이동 + Move Selection Home 선택 영역 처음으로 이동 + Move Selection Left 선택 영역 왼쪽으로 이동 + Move Selection Page Down 선택 영역 한 페이지 아래로 + Move Selection Page Up 선택 영역 한 페이지 위로 + Move Selection Right 선택 영역 오른쪽으로 이동 + Move Selection Up 선택 영역 위로 이동 + Move source clips to Processed Clips bin 소스 클립을 처리된 클립 빈으로 이동 + Movie 동영상 + Movie (audio and video) 동영상(오디오 및 비디오) + Movies, audio and stills from disk 디스크의 동영상, 오디오, 스틸 이미지 + MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps MPEG-4 오디오(.m4a), 스테레오 48 kHz AAC-LC 320 kbps + Multi-Camera 멀티 카메라 + Multi-Camera Audio Follows Video 멀티 카메라 오디오가 비디오 따라가기 + Multi-Camera Layout 멀티 카메라 레이아웃 + Multi-Camera Record 멀티 카메라 녹화 + Multi-Camera Record off 멀티 카메라 녹화 끔 + Multi-Camera Record on 멀티 카메라 녹화 켬 + Multi-Camera Record On/Off (0) 멀티 카메라 녹화 켬/끔(0) + Multi-Camera Record On/Off Toggle 멀티 카메라 녹화 켜기/끄기 + Multi-Camera Selection Top Down 멀티 카메라 위에서 아래로 선택 + Multi-camera source sequence with {n} camera(s) 카메라 {n}대 멀티 카메라 소스 시퀀스 + Multi-Camera View 멀티 카메라 보기 + Multiband Compressor 멀티밴드 컴프레서 + Multicam 멀티캠 + Multichannel Mono Media 멀티채널 모노 미디어 + Multiple Clips Selected 여러 클립 선택됨 + Multiplexer 멀티플렉서 + Multiply 곱하기 + Multitap Delay 멀티탭 딜레이 + Music 음악 + Mute 음소거 + Mute input during timeline recording 타임라인 녹음 중 입력 음소거 + Mute Send 전송 음소거 + MXF OP-Atom MXF OP-Atom + MXF OP-Atom DNxHR (Avid) MXF OP-Atom DNxHR(Avid) + MXF OP1a MXF OP1a + MXF OP1a DNxHR HQ MXF OP1a DNxHR HQ + MXF OP1a H.264 MXF OP1a H.264 + MXF OP1a ProRes 422 HQ MXF OP1a ProRes 422 HQ + MXF OP1a, DNxHR HQ (8-bit 4:2:2), 24-bit 48 kHz PCM, start timecode MXF OP1a, DNxHR HQ(8비트 4:2:2), 24비트 48 kHz PCM, 시작 타임코드 + MXF OP1a, H.264 High long GOP (0.3 bits per pixel), 24-bit 48 kHz PCM MXF OP1a, H.264 High 롱 GOP(픽셀당 0.3비트), 24비트 48 kHz PCM + MXF OP1a, ProRes 422 HQ, 24-bit 48 kHz PCM, start timecode MXF OP1a, ProRes 422 HQ, 24비트 48 kHz PCM, 시작 타임코드 + My Shortcuts 내 단축키 + My Templates 내 템플릿 + Name 이름 + Name (A–Z) 이름 (A–Z) + Name and role on a coloured slab that widens with the name. 이름에 따라 넓어지는 색상 판 위의 이름과 직함. + Name Color 이름 색상 + Name over a thin rule that always matches the name's width. 이름 너비에 항상 맞는 가는 선 위의 이름. + Name Overlay 이름 오버레이 + Name Size 이름 크기 + Name: 이름: + Narrow 좁게 + Navigate to End of Segment 세그먼트 끝으로 이동 + Navigate to Next Line 다음 줄로 이동 + Navigate to Next Word 다음 단어로 이동 + Navigate to Previous Line 이전 줄로 이동 + Navigate to Previous Word 이전 단어로 이동 + Navigate to Start of Segment 세그먼트 시작으로 이동 + Navigation 탐색 + Neon Color 네온 색상 + Neon Wipe 네온 닦아내기 + Nested Sequence Name 중첩 시퀀스 이름 + Nest… 중첩… + Network 네트워크 + Neutral Mono 중립 흑백 + New 새로 만들기 + New Bin 새 빈 + New Bin From Selection 선택 항목으로 새 빈 + New Color Matte 새 색상 매트 + New Ellipse 새 타원 + New Item 새 항목 + New Layer 새 레이어 + New Media Browser Panel 새 미디어 브라우저 패널 + New Offline File 새 오프라인 파일 + New Project Panel 새 프로젝트 패널 + New Project Shortcut 새 프로젝트 바로 가기 + New Rectangle 새 사각형 + New Search Bin 새 검색 빈 + New Sequence 새 시퀀스 + New Sequence From Clip 클립에서 새 시퀀스 + New Submix 새 서브믹스 + New Text 새 텍스트 + New Text Layer 새 텍스트 레이어 + New Workspace 새 작업 영역 + New {format} track 새 {format} 트랙 + Next Appearance Mode 다음 모양 모드 + Next in Sequence 시퀀스의 다음 항목 + Next in Track 트랙의 다음 항목 + Next Multi-Camera Page 다음 멀티 카메라 페이지 + Next page 다음 페이지 + Next to original media files 원본 미디어 파일 옆 + Next to the media 미디어 옆 + Next to the original media, in a Proxies folder 원본 미디어 옆 Proxies 폴더 + Night 야간 + Night Exterior 야간 실외 + No 아니요 + No background jobs. 백그라운드 작업 없음 + No candidates yet — Search a folder or Locate the file. 후보 없음 — 폴더를 검색하거나 파일을 찾으세요. + No captions on this track. Press + to add one at the playhead. 이 트랙에 자막 없음. +를 눌러 재생 헤드에 추가합니다. + no Chinese font is installed on this system; the interface stays in English 중국어 글꼴이 설치되어 있지 않아 인터페이스가 영어로 유지됩니다 + No contributor data was built into this copy. 이 사본에 기여자 데이터가 포함되지 않았습니다. + No effects 효과 없음 + No events. 이벤트 없음 + No export preset named `{name}` `{name}` 내보내기 사전 설정 없음 + No Fields (Progressive Scan) 필드 없음 (프로그레시브 스캔) + no GPU adapter: the CPU compositor is used GPU 어댑터 없음: CPU 합성기 사용 + no Japanese font is installed on this system; the interface stays in English 일본어 글꼴이 설치되어 있지 않아 인터페이스가 영어로 유지됩니다 + No markers. Press M to add one. 마커 없음. M을 눌러 추가합니다. + No matching captions. 일치하는 자막 없음. + No matching commands 일치하는 명령 없음 + No model credits were built into this copy. 이 사본에 모델 크레딧이 포함되지 않았습니다. + No Motion 모션 없음 + No multi-camera clip at the playhead 재생 헤드에 멀티 카메라 클립 없음 + No plugins are installed. Changes will take effect the next time you start FilmCraft. 설치된 플러그인이 없습니다. 변경 사항은 FilmCraft를 다음에 시작할 때 적용됩니다. + No saved guide templates. 저장된 안내선 템플릿 없음. + No Scroll 스크롤 없음 + No templates match. 일치하는 템플릿 없음. + Noise 노이즈 + Noise & Grain 노이즈 및 그레인 + Noise (Legacy) 노이즈(레거시) + Noise Level 노이즈 레벨 + Noise Reduction/Restoration 노이즈 감소/복원 + Noise Type 노이즈 유형 + Non-Additive Dissolve 비가산 디졸브 + None 없음 + Normal 표준 + Normalize All Peaks to: 모든 피크 정규화: + Normalize Max Peak to: 최대 피크 정규화: + Normalize Mix Track 믹스 트랙 노멀라이즈 + Normalize Mix Track to: 믹스 트랙 노멀라이즈 기준: + Normalize Mix Track… 믹스 트랙 정규화… + not available in this session 이 세션에서 사용할 수 없음 + Not matched yet 아직 일치 안 됨 + Not Now 나중에 + Not supported by FilmCraft's encoders yet FilmCraft 인코더에서 아직 지원 안 됨 + Not supported in FilmCraft yet FilmCraft에서 아직 지원되지 않음 + Notch 1 노치 1 + Notch 2 노치 2 + Notch 3 노치 3 + Notch 4 노치 4 + Notch 5 노치 5 + Notch 6 노치 6 + Notch Filter 노치 필터 + Notch Width 노치 폭 + Notes for this project: to-dos, instructions, frame numbers… Saved with the project. 이 프로젝트의 메모: 할 일, 지침, 프레임 번호… 프로젝트와 함께 저장됩니다. + Nothing found with that name. 해당 이름을 찾을 수 없습니다. + Nothing is assigned to this key 이 키에 할당된 항목 없음 + Nudge Clip Selection Down 클립 선택 아래로 조금 이동 + Nudge Clip Selection Left Five Frames 클립 선택 왼쪽으로 5프레임 이동 + Nudge Clip Selection Left One Frame 클립 선택 왼쪽으로 1프레임 이동 + Nudge Clip Selection Right Five Frames 클립 선택 오른쪽으로 5프레임 이동 + Nudge Clip Selection Right One Frame 클립 선택 오른쪽으로 1프레임 이동 + Nudge Clip Selection Up 클립 선택 위로 조금 이동 + Nudge Selected Object down by five 선택한 개체 아래로 5 이동 + Nudge Selected Object down by one 선택한 개체 아래로 1 이동 + Nudge Selected Object to left by five 선택한 개체 왼쪽으로 5 이동 + Nudge Selected Object to left by one 선택한 개체 왼쪽으로 1 이동 + Nudge Selected Object to right by five 선택한 개체 오른쪽으로 5 이동 + Nudge Selected Object to right by one 선택한 개체 오른쪽으로 1 이동 + Nudge Selected Object up by five 선택한 개체 위로 5 이동 + Nudge Selected Object up by one 선택한 개체 위로 1 이동 + Nudge Volume +1dB 볼륨 +1dB + Nudge Volume +3dB 볼륨 +3dB + Nudge Volume -1dB 볼륨 -1dB + Nudge Volume -3dB 볼륨 -3dB + Number of Audio Clips: 오디오 클립 수: + Number of Channels: 채널 수: + Number Of Echoes 에코 수 + Number of Harmonics 배음 수 + Numbered BMP stills (24-bit) 번호 매긴 BMP 스틸 (24비트) + Numbered DPX stills (10-bit RGB) 번호 매긴 DPX 스틸 (10비트 RGB) + Numbered JPEG stills (quality 90) 번호 매긴 JPEG 스틸 (품질 90) + Numbered OpenEXR stills (half float, linear, uncompressed) 번호 매긴 OpenEXR 스틸 (half float, 선형, 무압축) + Numbered PNG stills at the sequence's frame size 시퀀스 프레임 크기의 번호 매긴 PNG 스틸 + Numbered Targa stills (24-bit, uncompressed) 번호 매긴 Targa 스틸 (24비트, 무압축) + Numbered TIFF stills (RGB, 8-bit) 번호 매긴 TIFF 스틸 (RGB, 8비트) + Obsolete 사용 안 함 + Octagon 팔각형 + Off 끔 + Offline 오프라인 + Offline All 모두 오프라인 + Offline File… 오프라인 파일… + Offset 오프셋 + Offset (Turbulence) 오프셋(난류) + Offset Audio by: 오디오 오프셋: + Offset From 오프셋 시작 + Offset To 오프셋 끝 + Offset: 오프셋: + OK 확인 + OMF Export Settings OMF 내보내기 설정 + OMF Title: OMF 제목: + OMF… OMF… + Omni 무지향 + On 켬 + On Original Image 원본 이미지에 + On Transparent 투명 위에 + Opacity 불투명도 + Opacity 0 → 100 % over the first second 첫 1초 동안 불투명도 0 → 100 % + Opacity 100 → 0 % over the last second 마지막 1초 동안 불투명도 100 → 0 % + Open 열기 + Open a sequence to export 내보낼 시퀀스를 여세요 + Open a sequence to see its transcript 기록을 보려면 시퀀스를 여세요 + Open a sequence to work with captions 자막 작업을 하려면 시퀀스를 여세요 + Open Air 야외 + Open Bin 빈 열기 + Open Demo Project 데모 프로젝트 열기 + Open in New Tab 새 탭에서 열기 + Open in New Window 새 창에서 열기 + Open in new window 새 창에서 열기 + Open in Place 제자리에서 열기 + Open in place 제자리에서 열기 + Open In Source Monitor 소스 모니터에서 열기 + Open in Source Monitor 소스 모니터에서 열기 + Open in Timeline 타임라인에서 열기 + Open Ingest Settings 수집 설정 열기 + Open Most Recent 최근 항목 열기 + Open new tab 새 탭 열기 + Open Project… 프로젝트 열기… + Open Search 검색 열기 + Opened {url} {url} 열림 + OpenEXR OpenEXR + OpenEXR Sequence OpenEXR 시퀀스 + Opens up the shadows 그림자를 밝힙니다 + OpenTimelineIO… OpenTimelineIO… + OpenType OpenType + Optical Flow 광학 흐름 + Options 옵션 + Order 순서 + Ordering: 정렬: + Orientation 방향 + Orientation: 방향: + Out 끝 + out 끝 + Out Point 끝 표시(Out) + Out Points 끝 표시 + Out: {name} 끝: {name} + Outer 바깥쪽 + Outgoing 나가는 + Output 출력 + Output Gain 출력 게인 + Output Mapping 출력 매핑 + Outro Duration 아웃트로 길이 + Outside 외부 + Outside Color 외부 색상 + Overflow 오버플로 + Overlay 오버레이 + Override media colour space: 미디어 색 공간 재정의: + Overshoot 오버슈트 + Overwrite 덮어쓰기 + Overwrite (.) 덮어쓰기(.) + Overwrite Edit 덮어쓰기 편집 + Overwrite with Current View 현재 보기로 덮어쓰기 + Page Peel 페이지 벗기기 + Page Scroll 페이지 스크롤 + Page Turn 페이지 넘기기 + Page {page} of {pages} {page}/{pages} 페이지 + Paint Bucket 페인트 버킷 + Paint On Black 검정 위에 페인트 + Paint On Original Image 원본 이미지에 페인트 + Paint On Transparent 투명 위에 페인트 + Paint On White 흰색 위에 페인트 + Paint Style 페인트 스타일 + Paint Surface 페인트 표면 + Pan 팬 + Pan (Y Axis) 팬(Y축) + Pan / balance (drag; double-click: centre) 팬/밸런스(드래그, 더블클릭: 중앙) + Panel 패널 + Panel shortcuts override application shortcuts while that panel has focus 패널 단축키는 해당 패널 포커스 시 응용 프로그램 단축키보다 우선 + Panel Wipe 패널 닦아내기 + Panels 패널 + Panner 패너 + Panner - Ambisonics 패너 - Ambisonics + Parade 퍼레이드 + Parade (RGB) 퍼레이드(RGB) + Parade ({kind}) 퍼레이드({kind}) + Parade Type 퍼레이드 유형 + Paragraph Text 단락 텍스트 + Parametric Equalizer 파라메트릭 이퀄라이저 + Passband Ripple 통과 대역 리플 + Paste 붙여넣기 + Paste Attributes 속성 붙여넣기 + Paste Attributes… 특성 붙여넣기… + Paste Insert 삽입 붙여넣기 + Path 패스 + Path Points 패스 점 + Path: {path} 경로: {path} + Pause Media Encoder queue during playback 재생 중 Media Encoder 대기열 일시 중지 + Paused Resolution 일시 정지 해상도 + Peak Amplitude: {peak} 피크 진폭: {peak} + Peak Frequency 피크 주파수 + Peak Gain 피크 게인 + Pedestal 페데스탈 + Pen Tool 펜 도구 + Pentagon 오각형 + Per line 줄마다 + Percent Blur 흐림 비율 + Perspective 원근 + Phase Difference 위상차 + Phaser 페이저 + Phone Call 전화 통화 + Phosphor 형광체 + Phosphor Color 형광체 색상 + Photocopy 복사본 + Photocopy Color 복사본 색상 + Picture-in-picture: 35 % scale in the upper right 화면 속 화면: 오른쪽 위에 35 % 비율 + Pillow 필로우 + Pin All 모두 고정 + Pin Horizontal 가로 고정 + Pin Light 핀 라이트 + Pin To 고정 대상 + Pin Vertical 세로 고정 + Pinned Edges 고정된 가장자리 + Pinning 고정 + PiP 35% Upper Right PiP 35% 오른쪽 위 + Pitch Shifter 피치 시프터 + Pixel Aspect Ratio: 픽셀 종횡비: + Place files dropped onto the Timeline directly on the Timeline 타임라인에 놓은 파일을 타임라인에 바로 배치 + Placement 배치 + Placement: 배치: + Plate 플레이트 + Plateau Wipe 고원 닦아내기 + Plateaus 고원 + Plates 플레이트 + Play after rendering previews 미리 보기 렌더 후 재생 + Play Around 주변 재생 + Play Around Edit (Space loops, Shift+K once) 편집점 주변 재생(Space 반복, Shift+K 1회) + Play audio while scrubbing in Source and Program Monitors 소스 및 프로그램 모니터에서 스크러빙 시 오디오 재생 + Play from Playhead to Out Point 재생 헤드에서 끝 표시(Out)까지 재생 + Play In to Out 시작에서 끝까지 재생 + Play In to Out with Preroll/Postroll 프리롤/포스트롤 포함 시작에서 끝까지 재생 + Play Source 소스 재생 + Play-Stop Toggle (Space) 재생/정지 전환(Space) + Play/Stop 재생/정지 + Playback 재생 + Playback Resolution 재생 해상도 + Playback starts the pre-roll before the playhead (or the In point); with In/Out marked, recording punches in and out there. 재생 헤드(또는 시작 표시(In)) 앞 프리롤부터 재생합니다. 시작/끝이 지정되면 그 구간에서 펀치 인/아웃 녹음합니다. + Playback… 재생… + Playhead 재생 헤드 + Playhead position determines trim monitor loop playback 재생 헤드 위치로 트림 모니터 루프 재생 결정 + Playhead: 재생 헤드: + Plugins 플러그인 + Plugins… 플러그인… + PNG PNG + PNG or JPEG file PNG 또는 JPEG 파일 + PNG Sequence PNG 시퀀스 + Podcast Voice 팟캐스트 음성 + Point 1 점 1 + Point 1 Color 점 1 색상 + Point 1 Latitude 점 1 위도 + Point 1 Longitude 점 1 경도 + Point 2 점 2 + Point 2 Color 점 2 색상 + Point 2 Latitude 점 2 위도 + Point 2 Longitude 점 2 경도 + Point 3 점 3 + Point 3 Color 점 3 색상 + Point 3 Latitude 점 3 위도 + Point 3 Longitude 점 3 경도 + Point 4 점 4 + Point 5 점 5 + Point 6 점 6 + Point 7 점 7 + Point 8 점 8 + Point Text 포인트 텍스트 + Pointer Color 포인터 색상 + Points 점 + Policy 정책 + Polygon 다각형 + Polygon Sides 다각형 변 수 + Pop Motion 팝 모션 + Portuguese 포르투갈어 + Position 위치 + Position & Rotation 위치 및 회전 + Position Amount 위치 양 + Position, Scale & Rotation 위치, 비율 및 회전 + Position, Scale, Rotation 위치, 비율, 회전 + Position: 위치: + Position: bottom 위치: 아래 + Position: middle 위치: 가운데 + Position: top 위치: 위 + Post-Fader 페이더 후 + Post-roll 포스트롤 + Post-roll: 포스트롤: + Posterize 포스터화 + Posterize Time 시간 포스터화 + Postroll 포스트롤 + pre 프리 + Pre-Delay 프리딜레이 + Pre-Fader 페이더 전 + Pre-roll: 프리롤: + Preferences 설정 + Prefix 접두사 + Preroll 프리롤 + Presence Lift 프레즌스 리프트 + Preserve Luminosity 광도 유지 + Preserve Scale 비율 유지 + Preset 사전 설정 + Preset Manager 사전 설정 관리자 + Preset name 사전 설정 이름 + Preset: 사전 설정: + Presets 사전 설정 + Preview 미리보기 + Preview Area 미리 보기 영역 + Preview File Format: 미리 보기 파일 형식: + Previous in Sequence 시퀀스의 이전 항목 + Previous in Track 트랙의 이전 항목 + Previous Multi-Camera Page 이전 멀티 카메라 페이지 + Previous page 이전 페이지 + ProcAmp ProcAmp + Processing Options 처리 옵션 + Profile 프로필 + Program 프로그램 + Programme left → device output {l}, right → device output {r}{dev}. Outputs beyond the device's channels fall back to 1 and 2. 프로그램 왼쪽 → 장치 출력 {l}, 오른쪽 → 장치 출력 {r}{dev}. 장치 채널을 초과한 출력은 1, 2로 대체됩니다. + Progress 진행 상황 + Progressive 프로그레시브 + Project 프로젝트 + Project Auto Save: 프로젝트 자동 저장: + Project Manager 프로젝트 관리자 + Project Manager: writing {path} 프로젝트 관리자: {path} 쓰는 중 + Project Manager… 프로젝트 관리자… + Project Metadata 프로젝트 메타데이터 + Project Notes 프로젝트 메모 + Project Settings 프로젝트 설정 + Project View Preset {n} 프로젝트 보기 사전 설정 {n} + Project: {name} 프로젝트: {name} + Projecting cap 돌출 끝 + Projection: 투영: + ProjectPanel 프로젝트 패널 + Projects 프로젝트 + Project… 프로젝트… + Properties 속성 + Protect Highlights 하이라이트 보호 + Proxies are made in the background and attached when done. Export always uses full-resolution media. 프록시는 백그라운드에서 생성되어 완료 시 연결됩니다. 내보내기는 항상 원본 해상도 미디어를 사용합니다. + Proxy 프록시 + Pull Motion 당기기 모션 + Pulls highlights and whites down 밝은 영역과 흰색을 낮춥니다 + Purple 보라 + Push 밀기 + Push (Legacy) 밀기(레거시) + Q Q + Quality 품질 + Quality (CRF) 품질(CRF) + Queue ({n}) 대기열({n}) + Quick Export 빠른 내보내기 + Quick Export: open a sequence first 빠른 내보내기: 먼저 시퀀스를 여세요 + Quick Export: {path} 빠른 내보내기: {path} + QuickTime (Motion JPEG) QuickTime (Motion JPEG) + QuickTime, APV 422-10 (10-bit 4:2:2), 24-bit 48 kHz PCM QuickTime, APV 422-10 (10비트 4:2:2), 24비트 48 kHz PCM + QuickTime, APV 422-12 (12-bit 4:2:2), 24-bit 48 kHz PCM QuickTime, APV 422-12 (12비트 4:2:2), 24비트 48 kHz PCM + QuickTime, DNxHR HQ (8-bit 4:2:2), 24-bit 48 kHz PCM QuickTime, DNxHR HQ (8비트 4:2:2), 24비트 48 kHz PCM + QuickTime, DNxHR LB (8-bit 4:2:2), 24-bit 48 kHz PCM QuickTime, DNxHR LB (8비트 4:2:2), 24비트 48 kHz PCM + QuickTime, DNxHR SQ (8-bit 4:2:2), 24-bit 48 kHz PCM QuickTime, DNxHR SQ (8비트 4:2:2), 24비트 48 kHz PCM + QuickTime, ProRes 422 HQ, 24-bit 48 kHz PCM QuickTime, ProRes 422 HQ, 24비트 48 kHz PCM + QuickTime, ProRes 422 LT, 24-bit 48 kHz PCM QuickTime, ProRes 422 LT, 24비트 48 kHz PCM + QuickTime, ProRes 422 Proxy, 24-bit 48 kHz PCM QuickTime, ProRes 422 Proxy, 24비트 48 kHz PCM + QuickTime, ProRes 422, 24-bit 48 kHz PCM QuickTime, ProRes 422, 24비트 48 kHz PCM + QuickTime, ProRes 4444 (4:4:4) with alpha channel, 24-bit 48 kHz PCM QuickTime, ProRes 4444 (4:4:4) 알파 채널 포함, 24비트 48 kHz PCM + QuickTime, ProRes 4444 (4:4:4), 24-bit 48 kHz PCM QuickTime, ProRes 4444 (4:4:4), 24비트 48 kHz PCM + QuickTime, ProRes 4444 XQ (4:4:4), 24-bit 48 kHz PCM QuickTime, ProRes 4444 XQ (4:4:4), 24비트 48 kHz PCM + Quit FilmCraft FilmCraft 종료 + Radial 방사형 + Radial Blur 방사형 흐림 + Radial Ramp 방사형 램프 + Radial Wipe 방사형 닦아내기 + Radial Wipe (Legacy) 방사형 닦아내기(레거시) + Radio Announcer 라디오 아나운서 + Radio Voice 라디오 음성 + Radius 반경 + RAM reserved for other applications 다른 응용 프로그램용 예약 RAM + Ramp 램프 + Ramp Shape 램프 모양 + Random 무작위 + Random Seed 무작위 시드 + Range 범위 + Rate Stretch Tool 속도 조정 도구 + Ratio 비율 + Ray 광선 + Ray Color 광선 색상 + Ray Length 광선 길이 + Rays 광선 + Rays Only 광선만 + Razor Tool 자르기 도구 + Read 읽기 + Ready 준비됨 + Real name 실명 + Rec. 2100 Rec. 2100 + Rec. 601 Rec. 601 + Rec. 709 Rec. 709 + Recent Directories 최근 디렉터리 + Recognize when different speakers are talking 화자 구분 + Reconnect Full Resolution Media… 전체 해상도 미디어 다시 연결… + Record 녹화 + Record (voice-over on the record-enabled track) 녹음(녹음 활성 트랙에 내레이션) + Recorded {n} multi-camera cut(s) 멀티 카메라 컷 {n}개 녹화됨 + Recording voice-over… 보이스오버 녹음 중… + Recover 복구 + Recover Unsaved Changes 저장 안 된 변경 사항 복구 + Recover unsaved changes from {when}? {when}의 저장 안 된 변경 사항을 복구할까요? + Recover Unsaved Changes… 저장 안 된 변경 사항 복구… + Recovery copy: {dir} 복구 사본: {dir} + Rectangle 사각형 + Rectangle Tool 사각형 도구 + Red 빨강 + Red Blurriness 빨강 흐림 + Red Curve 빨강 곡선 + Red Offset 빨강 오프셋 + Red Power 빨강 파워 + Red Slope 빨강 기울기 + Red-Blue 빨강-파랑 + Red-Const 빨강-상수 + Red-Green 빨강-초록 + Red-Red 빨강-빨강 + Redo 다시 실행 + Reduce Axis 축 줄이기 + Reduce By 감소량 + Reduce Interlace Flicker 인터레이스 깜박임 줄이기 + Reduce Noise 노이즈 감소 + Reduce Reverb 잔향 감소 + Reduce Rumble 럼블 감소 + Reference 참조 + Reference {tc} 참조 {tc} + Reference Monitor 참조 모니터 + Reference track: 기준 트랙: + Reference: next frame 참조: 다음 프레임 + Reference: previous frame 참조: 이전 프레임 + Refine 다듬기 + Reflected 반사 + Reflection Angle 반사 각도 + Reflection Center 반사 중심 + Refraction 굴절 + Reframe Offset 리프레임 오프셋 + Reframe Zoom 리프레임 확대/축소 + Refresh 새로 고침 + Refresh Changed Media 변경된 미디어 새로 고침 + Refresh growing Files Every 증가 중인 파일 새로 고침 간격 + Refresh Project 프로젝트 새로 고침 + Refresh Sort Order 정렬 순서 새로 고침 + Rel 릴리스 + Relaxed 느슨함 + Release 릴리스 + Release Time 해제 시간 + Relief 부조 + Relink others automatically 나머지 자동 재연결 + Remaining 남음 + Remix 리믹스 + Remix Properties 리믹스 속성 + Remix Properties… 리믹스 속성… + Remix Tool 리믹스 도구 + Remixed to {seconds} s {seconds} s로 리믹스됨 + Remixing music to a target duration is not available yet. Trim the clip and add a crossfade instead. 음악을 목표 길이로 리믹스하는 기능은 아직 없습니다. 대신 클립을 트림하고 크로스페이드를 추가하세요. + Remove 제거 + Remove Attributes 속성 제거 + Remove Attributes… 특성 제거… + Remove Audio from AV Clip AV 클립에서 오디오 제거 + Remove Effect 효과 제거 + Remove Filler Words 군더더기 말 제거 + Remove filler words 군말 제거 + Remove Fringe 가장자리 색 번짐 제거 + Remove from Favorites 즐겨찾기에서 제거 + Remove from favorites 즐겨찾기에서 제거 + Remove keyframe 키프레임 제거 + Remove Media Cache Files 미디어 캐시 파일 제거 + Remove Pauses 일시 정지 구간 제거 + Remove Row 행 제거 + Remove Send 전송 제거 + Remove Unused 사용하지 않는 항목 제거 + Rename 이름 바꾸기 + Rename… 이름 바꾸기… + Render at Maximum Depth 최대 심도로 렌더 + Render Audio 오디오 렌더 + Render audio clip effects 오디오 클립 효과 렌더 + Render audio when rendering video 비디오 렌더 시 오디오도 렌더 + Render Edit in Audition files to: Audition에서 편집 파일 렌더 위치: + Render Effects In to Out 시작~끝 효과 렌더 + Render In to Out 시작~끝 렌더 + Render Selection 선택 영역 렌더 + Rendered preview 렌더된 미리 보기 + Renderer: 렌더러: + Rendering: 렌더링: + Repair 복구 + Repeat Edge Pixels 가장자리 픽셀 반복 + Replace Color 색상 바꾸기 + Replace Fonts in Projects 프로젝트의 글꼴 바꾸기 + Replace Fonts in Projects… 프로젝트 글꼴 바꾸기… + Replace With Clip 클립으로 바꾸기 + Replace With Clip From Source Monitor 소스 모니터의 클립으로 바꾸기 + Replace: 바꾸기: + Replaced {n} font uses 글꼴 사용 {n}곳 바꿈 + Replicate 복제 + Report an issue 문제 신고 + Report an Issue… 문제 신고… + Reset 재설정 + Reset All Parameters 모든 매개변수 재설정 + Reset Duration 지속 시간 재설정 + Reset Effect 효과 재설정 + Reset Parameter 매개변수 재설정 + Reset to Grid 격자로 재설정 + Reset to Saved Layout 저장된 레이아웃으로 재설정 + Reset… 재설정… + Resize floating panel 플로팅 패널 크기 조정 + Responsive Design - Position 반응형 디자인 - 위치 + Responsive Design - Time 반응형 디자인 - 시간 + Restore 복원 + Restore Arrangement 배열 복원 + Restore Captions from Source Clip 소스 클립에서 자막 복원 + Restore Frame 프레임 복원 + Restore open sequences when opening projects 프로젝트 열 때 열린 시퀀스 복원 + Restore Original 원래대로 복원 + Restore View Preset 보기 사전 설정 복원 + Restore Workspace 작업 영역 복원 + Restrict Trims To Subclip Boundaries 트림을 하위 클립 경계로 제한 + Result 결과 + Resulting Project 결과 프로젝트 + Retro 레트로 + Reveal in Finder Finder에서 보기 + Reveal in Project 프로젝트에서 표시 + Reveal Log Files… 로그 파일 표시… + Reveal Nested Sequence 중첩 시퀀스 표시 + Reveal Original Image 원본 이미지 표시 + Reveal Project in Finder… Finder에서 프로젝트 보기… + Reveal Sequence in Project 프로젝트에서 시퀀스 표시 + Reverb 잔향 + Reverb Amount 리버브 양 + Reverse 반전 + Reverse Field Dominance 필드 우선순위 반전 + Reverse Match Frame 역방향 프레임 일치 + Reverse Speed 역방향 속도 + Reverse the order 순서 반대로 + Revert 되돌리기 + Revert Remix 리믹스 되돌리기 + Revert to the preset's colors and names 사전 설정의 색상과 이름으로 되돌리기 + Review 검토 + RGB RGB + RGB Curves RGB 곡선 + RGB Split RGB 분할 + RGB-White RGB-흰색 + RGBA Channels RGBA 채널 + Right 오른쪽 + Right - Left 오른쪽 - 왼쪽 + Right - Right 오른쪽 - 오른쪽 + Right Align Text 텍스트 오른쪽 정렬 + Right align text 텍스트 오른쪽 정렬 + Right channel to device output 오른쪽 채널을 장치 출력으로 + Right to left 오른쪽에서 왼쪽 + Ripple Delete 리플 삭제 + Ripple Edit Tool 리플 편집 도구 + Ripple Edit, Shifting Trailing Clips 리플 편집, 뒤 클립 이동 + Ripple Sequence Markers 시퀀스 마커 리플 + Ripple trim adds edits to keep both sides of trim in sync 리플 트림 시 편집점을 추가해 양쪽 동기화 유지 + Ripple Trim In 시작 리플 트림 + Ripple Trim Next Edit to Playhead 다음 편집점을 재생 헤드까지 리플 트림 + Ripple Trim Out 끝 리플 트림 + Ripple Trim Previous Edit to Playhead 이전 편집점을 재생 헤드까지 리플 트림 + Role 직함 + Roll 롤 + Roll (Z Axis) 롤(Z축) + Roll Edit 롤 편집 + Rolling Edit Tool 롤 편집 도구 + Room Size 공간 크기 + Rose 로즈 + Rotation 회전 + Rotation Amount 회전량 + Roughen 거칠게 하기 + Roughen Color 거칠기 색상 + Roughen Edges 가장자리 거칠게 + Round cap 둥근 끝 + Round join 둥근 연결 + Rounded Crop 둥근 자르기 + Roundness 둥글기 + Rows 행 + Rule Color 선 색상 + Russian 러시아어 + Rusty 녹슨 + Rusty Color 녹슨 색상 + Safe graphics mode: {mode} 안전 그래픽 모드: {mode} + Safe Margins 세이프 마진 + Same as Project 프로젝트와 동일 + Sample Rate 샘플 레이트 + Sample Rate: 샘플 레이트: + Sample Size 샘플 크기 + Sat vs Sat 채도 대 채도 + Saturation 채도 + Saturation at 50 % 50%에서 채도 + Saturation Min 채도 최소 + Save 저장 + Save .cfa and .pek media cache files next to original media files when possible 가능하면 .cfa 및 .pek 미디어 캐시 파일을 원본 옆에 저장 + Save a Copy… 사본 저장… + Save All 모두 저장 + Save Arrangement 배열 저장 + Save Arrangement… 배열 저장… + Save As New View Preset 새 보기 사전 설정으로 저장 + Save as New Workspace… 새 작업 영역으로 저장… + Save as Template 템플릿으로 저장 + Save as Template… 템플릿으로 저장… + Save As… 다른 이름으로 저장… + Save Changes to this Workspace 이 작업 영역에 변경 사항 저장 + Save changes to “{project_name}” before closing? 닫기 전에 “{project_name}” 변경 사항을 저장할까요? + Save current settings as: 현재 설정 저장 이름: + Save Current View Preset 현재 보기 사전 설정 저장 + Save Guides as Template 안내선을 템플릿으로 저장 + Save Guides as Template… 안내선을 템플릿으로 저장… + Save Preset 사전 설정 저장 + Save preset as: 사전 설정 저장 이름: + Save Preset… 사전 설정 저장… + Save Project 프로젝트 저장 + Saved as a FilmCraft graphics template (.fcgt) in your templates folder. 템플릿 폴더에 FilmCraft 그래픽 템플릿(.fcgt)으로 저장했습니다. + Saved preset “{name}” 사전 설정 “{name}” 저장됨 + Saved template {path} 템플릿 저장됨: {path} + Saves the view, columns, sort, thumbnail and font size. 보기, 열, 정렬, 썸네일, 글꼴 크기를 저장합니다. + Scale 비율 + Scale (Degrees) 비율(도) + Scale Amount 비율 양 + Scale Attribute Times 속성 시간 비율 조정 + Scale From 비율 시작 + Scale Height 비율 높이 + Scale Match 비율 일치 + Scale motion effects proportionally when changing frame size 프레임 크기 변경 시 모션 효과 비율 맞춤 + Scale To 비율 끝 + Scale To Fill 채우기 비율 + Scale To Fit 맞춤 비율 + Scale to Frame Size 프레임 크기로 비율 조정 + Scale to frame size 프레임 크기로 비율 조정 + Scale Width 비율 너비 + Scaling 비율 조정 + Scan Lines 주사선 + Scene 장면 + Scene Edit Detection 장면 편집 감지 + Scene Edit Detection: analysing in the background (see Progress) 장면 편집 감지: 백그라운드에서 분석 중(진행률 참조) + Scene Edit Detection… 장면 편집 감지… + Scene: 장면: + Scientific Filter 과학 필터 + Scratch disk location for Captured Audio 캡처한 오디오의 스크래치 디스크 위치 + Scratch Disks 스크래치 디스크 + Scratch Disks… 스크래치 디스크… + Screen 스크린 + Search 검색 + Search Bin 빈 검색 + Search commands or keys 명령 또는 키 검색 + Search effects 효과 검색 + Search fields 검색 필드 + Search in: 검색 위치: + Search presets 사전 설정 검색 + Search templates 템플릿 검색 + Search: 검색: + second(s) 초 + Seconds 초 + seconds 초 + Segments 세그먼트 + Segments: 세그먼트: + Select 선택 + Select a clip to grade 색보정할 클립 선택 + Select a clip to see its properties 속성을 볼 클립 선택 + Select a clip to view its metadata. 클립을 선택하면 메타데이터가 표시됩니다. + Select a graphic clip, or use the Type tool (T) on the Program monitor 그래픽 클립을 선택하거나 프로그램 모니터에서 문자 도구(T) 사용 + Select a layer to edit its properties. 속성을 편집할 레이어를 선택하세요. + Select All 모두 선택 + Select All Matching 일치 항목 모두 선택 + Select All Project Items 모든 프로젝트 항목 선택 + Select an item to preview it 항목을 선택하면 미리 봅니다 + Select audio clips to edit them here 여기서 편집할 오디오 클립을 선택하세요 + Select audio clips, then click a preset to apply it. 오디오 클립을 선택한 뒤 사전 설정을 클릭해 적용합니다. + Select Camera 1 카메라 1 선택 + Select Camera 2 카메라 2 선택 + Select Camera 3 카메라 3 선택 + Select Camera 4 카메라 4 선택 + Select Camera 5 카메라 5 선택 + Select Camera 6 카메라 6 선택 + Select Camera 7 카메라 7 선택 + Select Camera 8 카메라 8 선택 + Select Camera 9 카메라 9 선택 + Select Clip at Playhead 재생 헤드 위치 클립 선택 + Select Find Box 찾기 상자 선택 + Select items to clear 지울 항목 선택 + Select Label Group 레이블 그룹 선택 + Select Next Clip 다음 클립 선택 + Select Next Graphic 다음 그래픽 선택 + Select Next Layer 다음 레이어 선택 + Select Next Panel 다음 패널 선택 + Select Previous Clip 이전 클립 선택 + Select Previous Graphic 이전 그래픽 선택 + Select Previous Layer 이전 레이어 선택 + Select Previous Panel 이전 패널 선택 + Select to Next Line 다음 줄까지 선택 + Select to Next Word 다음 단어까지 선택 + Select to Previous Line 이전 줄까지 선택 + Select to Previous Word 이전 단어까지 선택 + Select to Segment End 세그먼트 끝까지 선택 + Select to Segment Start 세그먼트 시작까지 선택 + Selected: {command} 선택됨: {command} + Selection as FilmCraft Project… 선택 항목을 FilmCraft 프로젝트로… + Selection Follows Playhead 재생 헤드 따라 선택 + Selection Order 선택 순서 + Selection Tool 선택 도구 + Selection… 선택 항목… + Semi-tones 반음 + Send Backward 뒤로 보내기 + Send exports here with Send to Queue, then start the queue. 대기열로 보내기로 내보내기를 추가한 뒤 대기열을 시작하세요. + Send to Back 맨 뒤로 보내기 + Send to Queue 대기열로 보내기 + Send {n} Level 보내기 {n} 레벨 + Sensitivity 감도 + Sensitivity: 감도: + Separate Audio 오디오 분리 + Separate Files 파일 분리 + Sepia Tone 세피아 톤 + Sequence 시퀀스 + Sequence Color Management 시퀀스 색상 관리 + Sequence From Clip 클립으로 시퀀스 만들기 + Sequence Name 시퀀스 이름 + Sequence name 시퀀스 이름 + Sequence Name: 시퀀스 이름: + Sequence Settings 시퀀스 설정 + Sequence Settings… 시퀀스 설정… + Sequence Time 시퀀스 시간 + Sequence Timecode 시퀀스 타임코드 + Sequence, {w}x{h}, {fps} fps, {rate} Hz 시퀀스, {w}x{h}, {fps} fps, {rate} Hz + Sequences 시퀀스 + Sequence… 시퀀스… + Sequentially 순차적으로 + Set 설정 + Set Comparison Reference 비교 기준 설정 + Set Creative Look 크리에이티브 룩 설정 + Set focus on the Timeline when performing Insert/Overwrite edits 삽입/덮어쓰기 편집 시 타임라인에 포커스 + Set Gain to: 게인 설정: + Set Input LUT 입력 LUT 설정 + Set Poster Frame 포스터 프레임 설정 + Set Project Panel View 프로젝트 패널 보기 설정 + Set the reference to the playhead 재생 헤드를 참조로 설정 + Set Timecode: 타임코드 설정: + Set to frame size 프레임 크기로 설정 + Set Transition Duration 전환 지속 시간 설정 + Setting 설정 + Settings 설정 + Settings: 설정: + SFX 효과음 + Shadow 그림자 + Shadow Angle 그림자 각도 + Shadow Blue Balance 그림자 파랑 균형 + Shadow Blur 그림자 흐림 + Shadow Color 그림자 색상 + Shadow Distance 그림자 거리 + Shadow Green Balance 그림자 초록 균형 + Shadow Only 그림자만 + Shadow Opacity 그림자 불투명도 + Shadow Red Balance 그림자 빨강 균형 + Shadow Size 그림자 크기 + Shadow Tint 그림자 색조 + Shadows 어두운 영역 + Shadows Lightness 그림자 밝기 + Shake 흔들기 + Shape 모양 + Shape Dissolve 모양 디졸브 + Shape Flow 모양 흐름 + Shape Size 모양 크기 + Shapes 모양 + Shard Size 파편 크기 + Sharp Colors 선명한 색상 + Sharpen 선명하게 + Sharpen Amount 선명하게 양 + Shift Center To 중심 이동 위치 + Shift clips that overlap trim point during ripple trimming 리플 트림 시 트림 지점과 겹치는 클립 이동 + Shortcut 단축키 + Shot 샷 + Shot/Take: 샷/테이크: + Show 표시 + Show "Project:" and "Bin:" in Project panel tab 프로젝트 패널 탭에 "프로젝트:" 및 "빈:" 표시 + Show Active Caption Tracks Only 활성 자막 트랙만 표시 + Show All 모두 표시 + Show All Caption Tracks 모든 자막 트랙 표시 + Show All Marker Colors 모든 마커 색상 표시 + Show Audio Waveform 오디오 파형 표시 + Show Clip Mismatch Warning dialog 클립 불일치 경고 대화 상자 표시 + Show clip names 클립 이름 표시 + Show Dot 점 표시 + Show durations 길이 표시 + Show Event Indicator 이벤트 표시기 표시 + Show finished jobs 완료된 작업 표시 + Show Focus Area 초점 영역 표시 + Show graphs 그래프 표시 + Show Guides 안내선 표시 + Show Home 홈 표시 + Show in the Preset menu 사전 설정 메뉴에 표시 + Show Keyframes 키프레임 표시 + Show Mask 마스크 표시 + Show Mask Tracker Preview 마스크 추적기 미리 보기 표시 + Show Multi-Camera Preview Monitor 멀티 카메라 미리보기 모니터 표시 + Show Next Screen 다음 화면 표시 + Show Only Noise 노이즈만 표시 + Show Open Dialog 열기 대화 상자 표시 + Show or hide {color} markers {color} 마커 표시/숨기기 + Show Previous Screen 이전 화면 표시 + Show Program Transcript 프로그램 대본 표시 + Show Project Load Error Dialog 프로젝트 로드 오류 대화 상자 표시 + Show Rich Tool Tips 상세 도구 설명 표시 + Show Role 직함 표시 + Show Rulers 눈금자 표시 + Show system compatibility issues at startup 시작 시 시스템 호환성 문제 표시 + Show the presets folder 사전 설정 폴더 표시 + Show Through Edits 연속 편집점 표시 + Show Tool Tips 도구 설명 표시 + Show Transport Controls 전송 컨트롤 표시 + Show Video Thumbnails 비디오 썸네일 표시 + Show Workspace Reset Warning dialog on double click 더블 클릭 시 작업 영역 재설정 경고 표시 + Show {n} {n} 표시 + Show/Hide Effects and Sends 효과 및 전송 표시/숨기기 + Show/Hide Tracks 트랙 표시/숨기기 + Show/Hide Tracks… 트랙 표시/숨기기… + Show: 표시: + Shrink 축소 + Shutter Angle 셔터 각도 + Shuttle Left 왼쪽으로 셔틀 + Shuttle Right 오른쪽으로 셔틀 + Shuttle Slow Left 왼쪽으로 느리게 셔틀 + Shuttle Slow Right 오른쪽으로 느리게 셔틀 + Shuttle Stop 셔틀 정지 + Signal Scale 신호 배율 + Silver Retention 은 잔류 + Similarity 유사성 + Simple Notch Filter 단순 노치 필터 + Simple Parametric EQ 단순 파라메트릭 EQ + Simple Text 단순 텍스트 + Simplify 단순화 + Simplify Sequence 시퀀스 단순화 + Simplify Sequence… 시퀀스 단순화… + Single-band Compressor 단일 대역 컴프레서 + Sinusoidal 사인파 + Six procedural 1080p clips with sound 사운드 포함 절차적 1080p 클립 6개 + Size 크기 + Size / Opacity 크기/불투명도 + Skew 기울이기 + Skew Axis 기울이기 축 + Slab Color 판 색상 + Slats 슬랫 + Slice 조각 + Slices 조각 + Slide 슬라이드 + Slide (Legacy) 슬라이드(레거시) + Slide Clip Selection Left Five Frames 클립 선택 왼쪽으로 5프레임 슬라이드 + Slide Clip Selection Left One Frame 클립 선택 왼쪽으로 1프레임 슬라이드 + Slide Clip Selection Right Five Frames 클립 선택 오른쪽으로 5프레임 슬라이드 + Slide Clip Selection Right One Frame 클립 선택 오른쪽으로 1프레임 슬라이드 + Slide Tool 슬라이드 도구 + Slip Clip Selection Left Five Frames 클립 선택 왼쪽으로 5프레임 슬립 + Slip Clip Selection Left One Frame 클립 선택 왼쪽으로 1프레임 슬립 + Slip Clip Selection Right Five Frames 클립 선택 오른쪽으로 5프레임 슬립 + Slip Clip Selection Right One Frame 클립 선택 오른쪽으로 1프레임 슬립 + Slip Tool 슬립 도구 + Slope 기울기 + Slow 느리게 + Slower Motion 더 느린 동작 + Small Caps 작은 대문자 + Small Room 작은 방 + Smaller 작게 + Smart Limit 스마트 제한 + Smart quotes 스마트 따옴표 + Smart Tools 스마트 도구 + Smooth 부드럽게 + Smooth Motion 부드러운 동작 + Smooth Scroll 부드러운 스크롤 + Smoothness 부드러움 + SMPTE HD bars with 1 kHz reference tone 1 kHz 기준 톤 포함 SMPTE HD 바 + Snap in Program Monitor 프로그램 모니터에 스냅 + Snap in Timeline 타임라인에 스냅 + Snap in Timeline (S) 타임라인 스냅(S) + Snap playhead in Timeline when Snap is enabled 스냅 사용 시 타임라인에서 재생 헤드 스냅 + Snap to grid 격자에 스냅 + Social Vertical 1080×1920 소셜 세로 1080×1920 + Soft 부드럽게 + Soft Clamp 소프트 클램프 + Soft Clip 소프트 클립 + Soft Edges (Best Quality) 부드러운 가장자리(최고 품질) + Soft Light 소프트 라이트 + Soft Vignette 부드러운 비네트 + Soft Wipe 소프트 와이프 + Soften 부드럽게 + Soften Harshness 거친 소리 완화 + Softness 부드러움 + software rendering 소프트웨어 렌더링 + Solarize 솔라리스 + Solid 단색 + Solid Colors 단색 + Solo 솔로 + Solo safe 솔로 보호 + Something went wrong while drawing the window. Your project is still open; save it to be safe. 창을 그리는 중 문제가 발생했습니다. 프로젝트는 열려 있으니 안전을 위해 저장하세요. + Sort 정렬 + Sort Icons 아이콘 정렬 + Sort Order 정렬 순서 + Source 소스 + Source channels the selected audio clips play: 선택한 오디오 클립이 재생할 소스 채널: + Source code on GitHub GitHub 소스 코드 + Source In/Out 소스 시작/끝 + Source Play/Stop 소스 재생/정지 + Source Point 소스 지점 + Source Settings 소스 설정 + Source Settings… 소스 설정… + Source Text 소스 텍스트 + Source Timecode 소스 타임코드 + Source · {name} 소스 · {name} + Source: 소스: + Spacer 스페이서 + Spanish 스페인어 + Speaker 화자 + Speaker Angle 스피커 각도 + Speaker Labeling 화자 레이블 지정 + Special 특수 + Special Effects 특수 효과 + Speech model 음성 모델 + Speech-to-text turns the dialogue into editable text. 음성 인식으로 대화를 편집 가능한 텍스트로 변환합니다. + Speed 속도 + Speed {n}% 속도 {n}% + Speed/Duration… 속도/지속 시간… + Speed: 속도: + Spherize 구형화 + Spiky 뾰족함 + Spill 번짐 + Spill Suppression 번짐 억제 + Spin 회전 + Spin Direction 회전 방향 + Spin Motion 회전 동작 + Split 분할 + Split (Legacy) 분할(레거시) + Split Caption 자막 분할 + Split caption at playhead 재생 헤드에서 자막 분할 + Spotlight 스포트라이트 + Spread 확산 + Spring Motion 스프링 동작 + Square 정사각형 + Square (Width Only) 정사각형(너비만) + Square 1:1 정사각형 1:1 + Square Pixels (1.0) 정사각형 픽셀 (1.0) + Stabilization 안정화 + Stabilize Only 안정화만 + Stabilize, Crop 안정화, 자르기 + Stabilize, Crop, Auto-scale 안정화, 자르기, 자동 비율 + Stabilize, Synthesize Edges 안정화, 가장자리 합성 + Stack 쌓기 + Stack Clips 클립 쌓기 + Stages 단계 + Standard 표준 + Star 별 + Star Wipe 별 와이프 + Start Angle 시작 각도 + Start at 0 0에서 시작 + Start at 1 1에서 시작 + Start Color 시작 색상 + Start Export Queue 내보내기 대기열 시작 + Start of Ramp 램프 시작 + Start Off Screen 화면 밖에서 시작 + Start Point 시작 지점 + Start Queue 대기열 시작 + Start Voice-over Recording 보이스오버 녹음 시작 + Start with an Empty Project 빈 프로젝트로 시작 + Start: 시작: + Starting Intensity 시작 강도 + Static Plates 정적 플레이트 + Status 상태 + Steel Blue 스틸 블루 + Step Back 1 Frame 1프레임 뒤로 + Step Back 1 Frame (Left) 1프레임 뒤로(Left) + Step Back Five Frames 5프레임 뒤로 + Step Back One Frame 1프레임 뒤로 + Step Forward 1 Frame 1프레임 앞으로 + Step Forward 1 Frame (Right) 1프레임 앞으로(Right) + Step Forward Five Frames 5프레임 앞으로 + Step Forward One Frame 1프레임 앞으로 + Step forward/back many 여러 프레임 앞/뒤로 이동 + Stereo 스테레오 + Stereo Expand 스테레오 확장 + Stereo Expander 스테레오 확장기 + Stereo Imagery 스테레오 이미지 + Stereo Media 스테레오 미디어 + Stereo Phasing 스테레오 위상 + Stereo Width 스테레오 폭 + Stereoscopic - Over/Under 입체 - 위/아래 + Stereoscopic - Side by Side 입체 - 좌우 + Still 정지 + Still Image Default Duration 스틸 이미지 기본 길이 + Stone Church 석조 교회 + Stop 중지 + Stop Export Queue 내보내기 대기열 중지 + Stop Multi-Camera Recording 멀티 카메라 녹화 중지 + Stop Source 소스 정지 + Stop tracking 추적 중지 + Stop Voice-over Recording 보이스오버 녹음 중지 + Stopband Attenuation 저지 대역 감쇠 + Straight Color 직선 색상 + Strength 강도 + Stretch 늘이기 + Stretch Gradient to Fit 그레이디언트를 맞게 늘이기 + Stretch Map to Fit 맵을 맞게 늘이기 + Stretch To Fill 늘려서 채우기 + Stretch to Fit Frame 프레임에 맞게 늘이기 + Stretch Width or Height 너비 또는 높이 늘이기 + Stretch Wipe 늘이기 와이프 + Stripe 줄무늬 + Stripes 줄무늬 + Strobe Color 스트로브 색상 + Strobe Duration (secs) 스트로브 지속 시간(초) + Strobe Light 스트로브 조명 + Strobe Period (secs) 스트로브 주기(초) + Stroke 선 + Stroke 2 선 2 + Stroke 2 Color 선 2 색상 + Stroke 2 Type 선 2 유형 + Stroke 2 Width 선 2 너비 + Stroke Angle 선 각도 + Stroke Color 선 색상 + Stroke Density 선 밀도 + Stroke Length 선 길이 + Stroke Length (secs) 선 길이(초) + Stroke line cap 선 끝 모양 + Stroke line join 선 연결 모양 + Stroke Only 선만 + Stroke Randomness 선 무작위성 + Stroke Type 선 유형 + Stroke Width 선 너비 + Studio Reverb 스튜디오 리버브 + Style 스타일 + Stylize 스타일화 + Subclip End 하위 클립 끝 + Subclip Start 하위 클립 시작 + Subclip: {name} 하위 클립: {name} + SubRip Subtitle (.srt) SubRip 자막(.srt) + Subspace Warp 하위 공간 왜곡 + Subtitle 부제 + Subtract 빼기 + Sum multichannel outputs to mono in Source Monitor 소스 모니터에서 다채널 출력을 모노로 합산 + Summary 요약 + Super Narrow 매우 좁음 + Surface Gloss 표면 광택 + Surface Material 표면 재질 + Surround Reverb 서라운드 리버브 + Swap 바꾸기 + Swap Channels 채널 바꾸기 + Switch Audio 오디오 전환 + Swivel 회전축 + Sync Voice-over Capture 보이스오버 캡처 동기화 + Sync with system 시스템과 동기화 + Synchronize Clips 클립 동기화 + Synchronize Point 동기화 지점 + Synchronized {n} clip group(s) 클립 그룹 {n}개 동기화됨 + Synchronize… 동기화… + Synthetic 합성 + System Compatibility Report 시스템 호환성 보고서 + System Compatibility Report… 시스템 호환성 보고서… + System Default 시스템 기본값 + System Language 시스템 언어 + Table 표 + Tag Color 태그 색상 + Tan 황갈색 + Tape 테이프 + Tape Name 테이프 이름 + Tape Name: 테이프 이름: + Tape/Tube 테이프/진공관 + Targa Targa + Targa Sequence Targa 시퀀스 + Target Aspect 대상 종횡비 + Target Bitrate 목표 비트 전송률 + Target Color 대상 색상 + Target Duration: 목표 길이: + Target Field of View 대상 시야각 + Target Loudness 대상 라우드니스 + Teal 청록 + Teal & Orange 틸 & 오렌지 + Teal Shadows, Warm Skin 틸 섀도, 따뜻한 피부톤 + Technical 기술 + Telephone 전화 + Teletext 텔레텍스트 + Temperature 색온도 + Template Name: 템플릿 이름: + Template Properties 템플릿 속성 + Temporal Interpolation 시간 보간 + Text 텍스트 + Text (graphics and captions) 텍스트(그래픽 및 자막) + Text Animator 텍스트 애니메이터 + Text Box Height 텍스트 상자 높이 + Text Box Width 텍스트 상자 너비 + Text Color 텍스트 색상 + Text Layer Type 텍스트 레이어 유형 + Text Properties 텍스트 속성 + Text Styling 텍스트 스타일 + Text to Speech 텍스트 음성 변환 + TextPanel 텍스트 패널 + The default graphics are used from the next start. 다음 시작부터 기본 그래픽을 사용합니다. + The language changed but could not be saved: {e} 언어가 변경되었으나 저장하지 못했습니다: {e} + The media cache holds render previews of unsaved projects and other files FilmCraft can recreate. Deleting them is always safe; FilmCraft rebuilds them as needed. 미디어 캐시에는 저장되지 않은 프로젝트의 렌더 미리 보기 등 FilmCraft가 다시 만들 수 있는 파일이 있습니다. 삭제해도 안전하며 필요 시 FilmCraft가 다시 생성합니다. + The project had not been saved yet. 프로젝트가 아직 저장되지 않았습니다. + The selected clips have different audio types. Assign one type to edit them together. 선택한 클립의 오디오 유형이 다릅니다. 함께 편집하려면 한 유형을 지정하세요. + The sequence has no caption track. 시퀀스에 자막 트랙이 없습니다. + Thickness 두께 + This build has no speech-to-text; import a transcript with transcript.set. 이 빌드에는 음성 인식이 없습니다. transcript.set으로 기록을 가져오세요. + Thr 임계 + Threshold 임계값 + Thumbnail controls for all pointing devices 모든 포인팅 장치용 썸네일 컨트롤 + Thumbnail Size Next 다음 썸네일 크기 + Thumbnail Size Previous 이전 썸네일 크기 + Thumbnail View 썸네일 보기 + Thumbnails 썸네일 + Thumbnails show effects applied 썸네일에 적용된 효과 표시 + Ticker – Crawl 티커 – 크롤 + TIFF TIFF + TIFF Sequence TIFF 시퀀스 + Tile Gradient 그레이디언트 타일 + Tilt 기울이기 + Tilt (X Axis) 기울이기(X축) + Time 시간 + Time and Pitch 시간 및 피치 + Time Interpolation 시간 보간 + Time Remapping 시간 재매핑 + Timebase: 시간 기준: + Timecode 타임코드 + Timecode Conversion 타임코드 변환 + Timecode Overlay 타임코드 오버레이 + Timecode: 타임코드: + Timecode… 타임코드… + Timeline 타임라인 + Timeline Display Settings 타임라인 표시 설정 + Timeline Mouse Scrolling 타임라인 마우스 스크롤 + Timeline Playback Auto-Scrolling 타임라인 재생 자동 스크롤 + Timeline: (no sequences) 타임라인: (시퀀스 없음) + Timeline… 타임라인… + Tint 색조 + Tint Color 색조 색상 + Title 제목 + Title Color 제목 색상 + Title Safe Area 제목 안전 영역 + Title Size 제목 크기 + Title – Boxed 제목 – 상자 + Title – Centered 제목 – 가운데 + Titles 제목 + To 끝 + To {name} {name}(으)로 + Toggle All Audio Targets 모든 오디오 대상 전환 + Toggle All Source Audio 모든 소스 오디오 전환 + Toggle All Source Video 모든 소스 비디오 전환 + Toggle All Video Targets 모든 비디오 대상 전환 + Toggle animation 애니메이션 전환 + Toggle Audio During Scrubbing 스크러빙 중 오디오 전환 + Toggle Full Screen 전체 화면 전환 + Toggle Mute for All Targeted Audio Tracks 대상 오디오 트랙 모두 음소거 전환 + Toggle Proxies 프록시 전환 + Toggle Solo for All Targeted Audio Tracks 대상 오디오 트랙 모두 솔로 전환 + Toggle Source/Program Monitor Focus 소스/프로그램 모니터 포커스 전환 + Toggle Target Audio 1 대상 오디오 1 전환 + Toggle Target Audio 2 대상 오디오 2 전환 + Toggle Target Audio 3 대상 오디오 3 전환 + Toggle Target Audio 4 대상 오디오 4 전환 + Toggle Target Audio 5 대상 오디오 5 전환 + Toggle Target Audio 6 대상 오디오 6 전환 + Toggle Target Audio 7 대상 오디오 7 전환 + Toggle Target Audio 8 대상 오디오 8 전환 + Toggle Target Video 1 대상 비디오 1 전환 + Toggle Target Video 2 대상 비디오 2 전환 + Toggle Target Video 3 대상 비디오 3 전환 + Toggle Target Video 4 대상 비디오 4 전환 + Toggle Target Video 5 대상 비디오 5 전환 + Toggle Target Video 6 대상 비디오 6 전환 + Toggle Target Video 7 대상 비디오 7 전환 + Toggle Target Video 8 대상 비디오 8 전환 + Toggle Track Output for All Targeted Video Tracks 대상 비디오 트랙 모두 출력 전환 + Toggle Trim Type 트림 유형 전환 + Toggle View 보기 전환 + Tolerance 허용치 + Tone 톤 + Tools 도구 + Top 위쪽 + Top Center 위쪽 가운데 + Top Left 왼쪽 위 + Top Right 오른쪽 위 + Total Duration 총 길이 + Touch 터치 + Track Fx Editor 트랙 Fx 편집기 + Track Keyframes 트랙 키프레임 + Track Matte Key 트랙 매트 키 + Track Select Backward Tool 뒤로 트랙 선택 도구 + Track Select Forward Tool 앞으로 트랙 선택 도구 + Track Selected Mask 선택한 마스크 추적 + Track selected mask backward 선택한 마스크 뒤로 추적 + Track selected mask backward 1 frame 선택한 마스크 뒤로 1프레임 추적 + Track selected mask forward 선택한 마스크 앞으로 추적 + Track selected mask forward 1 frame 선택한 마스크 앞으로 1프레임 추적 + Track type 트랙 유형 + Tracking 자간 + Tracking method: {method} 추적 방식: {method} + Tracks 트랙 + tracks 트랙 + Transcode 트랜스코딩 + Transcribe 기록 + Transcribe Sequence 시퀀스 텍스트 변환 + Transcribe sequence 시퀀스 기록 + Transcribe Sequence… 시퀀스 받아쓰기… + Transcribe… 받아쓰기… + Transcript 대본 + Transcription 트랜스크립션 + Transcription preferences 트랜스크립션 설정 + Transform 변형 + Transformers 트랜스포머 + Transience 트랜지언스 + Transition Completion 전환 완료 + Transition Softness 전환 부드러움 + Transitions 전환 + Transmit Multi-Camera View 멀티 카메라 보기 전송 + Transparency 투명도 + Transparent Video… 투명 비디오… + Trash 휴지통 + Travel Motion 이동 동작 + Treble 고음 + Triangle 삼각형 + Trim 트림 + Trim audio files 오디오 파일 트림 + Trim Backward 뒤로 트림 + Trim Backward Many 여러 프레임 뒤로 트림 + Trim Edit 트림 편집 + Trim Forward 앞으로 트림 + Trim Forward Many 여러 프레임 앞으로 트림 + Trim In 시작 트림 + Trim Next Edit to Playhead 다음 편집점을 재생 헤드까지 트림 + Trim Out 끝 트림 + Trim Previous Edit to Playhead 이전 편집점을 재생 헤드까지 트림 + Trimming 트림 + Trim… 트림… + True Peak Limit 트루 피크 제한 + Tube 진공관 + Tube-modeled Compressor 진공관 모델 컴프레서 + Tubular 관형 + Turbulent 난류 + Turbulent Displace 난류 변위 + Turbulent Sharp 난류 날카롭게 + Turbulent Smooth 난류 부드럽게 + Turbulent Smoother 난류 더 부드럽게 + Turn on animation and add keyframe 애니메이션 켜고 키프레임 추가 + TV Power TV 전원 + Tweed 트위드 + Twirl 소용돌이 + Twirl (Legacy) 소용돌이(레거시) + Twirl Center 소용돌이 중심 + Twirl Radius 소용돌이 반경 + Twist 비틀기 + Twist Smoother 비틀기 더 부드럽게 + Type 유형 + Type a shortcut… (Esc cancels) 단축키 입력…(Esc로 취소) + Type Tool 문자 도구 + Type: 유형: + Typewriter 타자기 + UI Scale UI 배율 + UI: UI: + Ultra Key 울트라 키 + Uncompressed (PCM) 비압축(PCM) + Underline 밑줄 + Undo 실행 취소 + Undock Panel 패널 도킹 해제 + Ungroup 그룹 해제 + Uniform Scale 균일 비율 + Uniform Spacing 균일 간격 + Universal Counting Leader… 범용 카운트다운 리더… + Unlink 연결 해제 + Unreadable 읽을 수 없음 + Unrendered: render to play back in real time 렌더 안 됨: 실시간 재생하려면 렌더 필요 + Unrendered: should play back in real time 렌더 안 됨: 실시간 재생 가능 + Unsharp Mask 언샤프 마스크 + Unstack 쌓기 해제 + Unstack Clips 클립 쌓기 해제 + Untagged 태그 없음 + Untitled 제목 없음 + Untitled Workspace 제목 없는 작업 영역 + Up One Level 한 단계 위로 + Up one level 한 단계 위로 + Update it at least every 최소 업데이트 간격 + Update Metadata 메타데이터 업데이트 + Update Metadata… 메타데이터 업데이트… + Update timeline dynamically during ripple edits. 리플 편집 중 타임라인 동적 업데이트. + Upgrade Caption to Graphic 자막을 그래픽으로 변환 + Upgrade to Source Graphic 소스 그래픽으로 변환 + Upper First 위쪽 필드 먼저 + Upper Frequency 상한 주파수 + Upper Left 왼쪽 위 + Upper Right 오른쪽 위 + Use Color Noise 색상 노이즈 사용 + Use Default Graphics 기본 그래픽 사용 + Use Effect Shutter Angle 효과 셔터 각도 사용 + Use End Color 끝 색상 사용 + Use File 파일 사용 + Use file metadata 파일 메타데이터 사용 + Use Glow Color 광선 색상 사용 + Use Maximum Render Quality 최고 렌더 품질 사용 + Use Media Source 미디어 소스 사용 + Use the file's timecode 파일 타임코드 사용 + Use Tint Color 색조 색상 사용 + User 사용자 + User Order 사용자 순서 + Username 사용자 이름 + Utility 유틸리티 + Validate Content Credentials on import 가져올 때 콘텐츠 자격 증명 확인 + Value 값 + Variations: 변형: + VBR, 1 pass VBR, 1패스 + VBR, 2 pass VBR, 2패스 + Vectorscope (YUV) 벡터스코프(YUV) + Vectorscope + Waveform YC 벡터스코프 + 파형 YC + Vectorscope HLS 벡터스코프 HLS + Vectorscope Targets 벡터스코프 대상 + Vectorscope YUV 벡터스코프 YUV + Velocity 속력 + Venetian Blinds 베니션 블라인드 + Version 버전 + Version {version} 버전 {version} + Vertical 세로 + vertical 세로 + Vertical 4:5 세로 4:5 + Vertical 9:16 세로 9:16 + Vertical Alignment 세로 정렬 + Vertical Blocks 세로 블록 + Vertical Decentering 세로 중심 이탈 + Vertical Displacement 세로 변위 + Vertical Flip 세로 뒤집기 + Vertical Margin 세로 여백 + Vertical Text 세로 텍스트 + Vertical Type Tool 세로 문자 도구 + Very Narrow 아주 좁음 + VHS Damage VHS 손상 + Vibrance 활기 + Video 비디오 + Video 1 비디오 1 + Video 2 비디오 2 + Video 3 비디오 3 + Video 4 비디오 4 + Video 5 비디오 5 + Video 6 비디오 6 + Video 7 비디오 7 + Video 8 비디오 8 + Video and Audio 비디오 및 오디오 + Video and Audio Waveform Split 비디오/오디오 파형 분할 + Video and Waveform 비디오 및 파형 + Video Attributes 비디오 속성 + Video Codec 비디오 코덱 + Video Duration 비디오 길이 + Video Effects 비디오 효과 + Video effects 비디오 효과 + Video Frame 비디오 프레임 + Video In 비디오 입력 + Video In Point 비디오 시작 표시(In) + Video Info 비디오 정보 + Video Limiter 비디오 리미터 + Video Only 비디오만 + Video Options 비디오 옵션 + Video Out 비디오 출력 + Video Out Point 비디오 끝 표시(Out) + Video Previews 비디오 미리 보기 + Video Previews: 비디오 미리 보기: + Video Rendering and Playback 비디오 렌더링 및 재생 + Video Thumbnails 비디오 썸네일 + Video Tracks 비디오 트랙 + Video Transition Default Duration 비디오 전환 기본 길이 + Video Transitions 비디오 전환 + Video Usage 비디오 사용 + Video Usage: {vu} Audio Usage: {au} 비디오 사용: {vu} 오디오 사용: {au} + Video {n}: 비디오 {n}: + Video: 비디오: + View 보기 + Vignette 비네트 + Vimeo 1080p Full HD Vimeo 1080p Full HD + Vimeo 2160p 4K Ultra HD Vimeo 2160p 4K Ultra HD + Violet 바이올렛 + Vivid Light 선명한 라이트 + Vocal Booth 보컬 부스 + Vocal Enhancer 보컬 향상 + Voice 음성 + Voice in a Hall 홀 속 음성 + Voice-over Record 보이스오버 녹음 + Voice-over record (right-click: Voice-Over Record Settings…) 보이스오버 녹음(오른쪽 클릭: 보이스오버 녹음 설정…) + Voice-Over Record Settings 보이스오버 녹음 설정 + Voice-Over Record Settings… 보이스오버 녹음 설정… + Voice-over recorded: {p} 보이스오버 녹음됨: {p} + Voice-over recording stopped (nothing recorded) 보이스오버 녹음 중지됨(녹음 내용 없음) + Voice-over take discarded 보이스오버 테이크 삭제됨 + Voiceover 보이스오버 + Volume 볼륨 + Volume (drag; double-click: 0 dB) 볼륨(드래그, 더블클릭: 0 dB) + Volumetric Rays 볼륨 광선 + VR Blur VR 흐림 + VR Chroma Leaks VR 색 번짐 + VR Chromatic Aberrations VR 색수차 + VR Color Gradients VR 색상 그레이디언트 + VR De-Noise VR 노이즈 제거 + VR Digital Glitch VR 디지털 글리치 + VR Fractal Noise VR 프랙탈 노이즈 + VR Glow VR 광선 + VR Gradient Wipe VR 그레이디언트 와이프 + VR Iris Wipe VR 아이리스 와이프 + VR Light Leaks VR 빛 번짐 + VR Light Rays VR 빛줄기 + VR Mobius Zoom VR 뫼비우스 확대/축소 + VR Plane to Sphere VR 평면을 구로 + VR Projection VR 투영 + VR Properties VR 속성 + VR Random Blocks VR 무작위 블록 + VR Rotate Sphere VR 구 회전 + VR Sharpen VR 선명하게 + VR Spherical Blur VR 구형 흐림 + Warm Film 따뜻한 필름 + Warm highlights, brown shadows 따뜻한 하이라이트, 갈색 섀도 + Warm Negative 따뜻한 네거티브 + Warm print stock with gentle roll-off 부드러운 롤오프의 따뜻한 인화 필름 + Warm Room 따뜻한 방 + Warm Tint 따뜻한 색조 + Warm Voice 따뜻한 음성 + Warm, soft late-afternoon light 따뜻하고 부드러운 늦은 오후 빛 + Warning 경고 + Warnings and Errors 경고 및 오류 + Warp Stabilizer 워프 안정기 + WAV, stereo 48 kHz 16-bit PCM WAV, 스테레오 48 kHz 16비트 PCM + WAV, stereo 48 kHz 24-bit PCM WAV, 스테레오 48 kHz 24비트 PCM + Wave 파형 + Wave Height 파동 높이 + Wave Speed 파동 속도 + Wave Warp 파동 왜곡 + Wave Width 파동 너비 + Waveform 파형 + Waveform (Luma) 파형(루마) + Waveform (RGB) 파형(RGB) + Waveform ({kind}) 파형({kind}) + Waveform Audio 파형 오디오 + Waveform Audio 48 kHz 16-bit 파형 오디오 48 kHz 16비트 + Waveform Audio 48 kHz 24-bit 파형 오디오 48 kHz 24비트 + Waveform Type 파형 유형 + Wavelength 파장 + Web & Social 웹 및 소셜 + WebVTT (.vtt) WebVTT(.vtt) + Wet 웻 + Wet Out 웻 출력 + When Opening a Project 프로젝트 열 때 + Whip 휩 + Whip (Legacy) 휩(레거시) + Whisper base (balanced) Whisper base(균형) + Whisper small (most accurate) Whisper small(가장 정확) + Whisper tiny (fastest) Whisper tiny(가장 빠름) + White 흰색 + White Input Level 흰색 입력 레벨 + White Is High 흰색이 높음 + White/Black 흰색/검정 + Whites 흰색 + Wide gamut color (composite in BT.2020) 광색역 색상(BT.2020으로 합성) + Wide Outdoor 넓은 야외 + Widener 와이드너 + Width 너비 + Width: 너비: + Wiggle 흔들기 + Window 창 + Wipe 와이프 + Wipe Angle 와이프 각도 + with: 사용: + Without keyframes 키프레임 없이 + Wonder Glow 원더 글로우 + Work Area 작업 영역 + Working Color Space 작업 색 공간 + Working Color Space: 작업 색 공간: + Workspaces 작업 영역 + Wrap Back 뒤로 감기 + Write 쓰기 + Write Automation 자동화 쓰기 + Write clip markers to XMP 클립 마커를 XMP에 쓰기 + Write keyframes 키프레임 쓰기 + Write the metadata of the selected clip(s) ({n}) to XMP files next to their media? 선택한 클립({n})의 메타데이터를 미디어 옆 XMP 파일에 쓸까요? + Write XMP ID to files on import 가져올 때 파일에 XMP ID 쓰기 + Write-on 쓰기 효과 + X Rotation X 회전 + Y Rotation Y 회전 + YC YC + YC no Chroma YC(크로마 없음) + Yellow 노랑 + Yellow-green highlights and blue-lifted shadows from crossed channel curves 교차 채널 커브로 황록색 하이라이트와 푸른빛 섀도 + Yes 예 + YouTube 1080p Full HD YouTube 1080p Full HD + YouTube 2160p 4K Ultra HD YouTube 2160p 4K Ultra HD + YUV YUV + Z Position Z 위치 + Z Rotation Z 회전 + ZIP (1 scan line) ZIP (스캔 라인 1개) + ZIP (16 scan lines) ZIP (스캔 라인 16개) + Zoom 확대/축소 + Zoom Amount 확대/축소 양 + Zoom Blur 확대 흐림 + Zoom In 확대 + Zoom Out 축소 + Zoom Program Monitor to 100% 프로그램 모니터 100%로 확대/축소 + Zoom Program Monitor to Fit 프로그램 모니터 맞춤 확대/축소 + Zoom Source Monitor to 100% 소스 모니터 100%로 확대/축소 + Zoom Source Monitor to Fit 소스 모니터 맞춤 확대/축소 + Zoom Strength 확대 강도 + Zoom to Sequence 시퀀스에 맞게 확대/축소 + Zoom Tool 확대/축소 도구 + {active} (modified) {active}(수정됨) + {category} · My Template {category} · 내 템플릿 + {codec} · {rate} Hz · {n} channel(s) {codec} · {rate} Hz · {n}채널 + {first} - Merged {first} - 병합됨 + {first} Multicam {first} 멀티캠 + {fps} fps · {n} frames queued {fps} fps · 대기 프레임 {n}개 + {f} (missing) {f}(누락) + {key} can't be used as a shortcut {key}은(는) 단축키로 사용할 수 없음 + {mode} · no clip {mode} · 클립 없음 + {mode} · no sequence {mode} · 시퀀스 없음 + {name} (copy) {name}(사본) + {name} (Modified) {name} (수정됨) + {name} (Simplified) {name}(단순화) + {name} (user) {name}(사용자) + {name} - Edited {name} - 편집됨 + {name} Selection.fcproj {name} 선택.fcproj + {n} clip(s) can't find their media. Locate them, search a folder, or leave them offline. 클립 {n}개의 미디어를 찾을 수 없습니다. 찾기, 폴더 검색 또는 오프라인 유지를 선택하세요. + {n} clip(s) selected. 클립 {n}개 선택됨. + {n} clip(s) selected; the clip on the reference track stays put. 클립 {n}개 선택됨. 기준 트랙의 클립은 고정됩니다. + {n} clip(s): {names} 클립 {n}개: {names} + {n} Color {n} 색상 + {n} editable properties 편집 가능 속성 {n}개 + {n} files 파일 {n}개 + {n} Font {n} 글꼴 + {n} h ago {n}시간 전 + {n} items 항목 {n}개 + {n} min ago {n}분 전 + {n} Position {n} 위치 + {n} s ago {n}초 전 + {n} sessions have unsaved changes: 세션 {n}개에 저장 안 된 변경 사항이 있습니다: + {n} Size {n} 크기 + {n}. (empty) {n}. (비어 있음) + {panel} Panel {panel} 패널 + {panel} — coming in a later milestone {panel} — 이후 마일스톤에서 제공 + {rate} fps Drop-Frame Timecode {rate} fps 드롭 프레임 타임코드 + {rate} fps Non Drop-Frame Timecode {rate} fps 논드롭 프레임 타임코드 + {rate} fps Timecode {rate} fps 타임코드 + {rate} frames/second {rate}프레임/초 + {sel} of {total} items selected {total}개 중 {sel}개 항목 선택됨 + {tc} Chapter {tc} 챕터 + {total} items 항목 {total}개 + ● Recording ● 녹음 중 + ✓ Enable ✓ 사용 + ✔ matches ✔ 일치 + ✔ same file ✔ 같은 파일 + ✖ different file ✖ 다른 파일 diff --git a/crates/ui-egui/src/i18n/ko.tsv.attribution b/crates/ui-egui/src/i18n/ko.tsv.attribution new file mode 100644 index 00000000..ac70cd49 --- /dev/null +++ b/crates/ui-egui/src/i18n/ko.tsv.attribution @@ -0,0 +1,8 @@ +asset: ko.tsv +title: Korean interface translation catalog +author: FilmCraft contributors +source: original work +license: MIT OR Apache-2.0 +license-file: LICENSE-MIT and LICENSE-APACHE +added: 2026-10-13 +notes: Original English-to-Korean interface translations using clean-room NLE vocabulary. diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 9dd1ad9b..32cd9b3d 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -46,6 +46,8 @@ pub enum Language { Ru, Fr, Id, + /// Korean; Hangul needs an installed system font ([`korean_font_available`]). + Ko, } static JAPANESE: OnceLock = OnceLock::new(); @@ -56,10 +58,11 @@ static CHINESE: OnceLock = OnceLock::new(); static GERMAN: OnceLock = OnceLock::new(); static RUSSIAN: OnceLock = OnceLock::new(); static FRENCH: OnceLock = OnceLock::new(); +static KOREAN: OnceLock = OnceLock::new(); static INDONESIAN: OnceLock = OnceLock::new(); impl Language { - pub const ALL: [Self; 10] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id]; + pub const ALL: [Self; 11] = [Self::En, Self::Ja, Self::Es, Self::PtBr, Self::Uk, Self::ZhCn, Self::De, Self::Ru, Self::Fr, Self::Id, Self::Ko]; pub fn name(self) -> &'static str { match self { @@ -73,6 +76,7 @@ impl Language { Self::Ru => "Русский", Self::Fr => "Français", Self::Id => "Bahasa Indonesia", + Self::Ko => "한국어", } } @@ -88,6 +92,7 @@ impl Language { "ru" => Some(Self::Ru), "fr" => Some(Self::Fr), "id" => Some(Self::Id), + "ko" => Some(Self::Ko), _ => None, } } @@ -101,7 +106,7 @@ impl Language { /// only for Simplified locales (`zh`, `zh-CN`, `zh-SG`, `zh-Hans…`); Traditional ones (`zh-TW`, /// `zh-HK`, `zh-MO`, `zh-Hant…`) are skipped. pub fn from_locales(tags: &[String]) -> Self { - const PRIMARY: [(&str, Language); 10] = [ + const PRIMARY: [(&str, Language); 11] = [ ("en", Language::En), ("ja", Language::Ja), ("es", Language::Es), @@ -112,6 +117,7 @@ impl Language { ("fr", Language::Fr), ("id", Language::Id), ("in", Language::Id), + ("ko", Language::Ko), ]; tags.iter() .find_map(|tag| { @@ -142,6 +148,7 @@ impl Language { Self::Ru => "ru", Self::Fr => "fr", Self::Id => "id", + Self::Ko => "ko", } } @@ -158,6 +165,7 @@ impl Language { Self::Ru => Some(RUSSIAN.get_or_init(|| Catalog::parse(include_str!("ru.tsv")))), Self::Fr => Some(FRENCH.get_or_init(|| Catalog::parse(include_str!("fr.tsv")))), Self::Id => Some(INDONESIAN.get_or_init(|| Catalog::parse(include_str!("id.tsv")))), + Self::Ko => Some(KOREAN.get_or_init(|| Catalog::parse(include_str!("ko.tsv")))), } } @@ -313,6 +321,13 @@ pub fn chinese_font_available() -> bool { crate::cjk::available() } +/// Whether the interface can show Korean: `theme::install` adds an installed system Hangul face +/// (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…, see `cjk.rs`) when there is one; none +/// is bundled. False on systems (and the web) without one. +pub fn korean_font_available() -> bool { + crate::cjk::korean_available() +} + #[cfg(test)] mod tests { use super::*; @@ -329,6 +344,7 @@ mod tests { ("ru", include_str!("ru.tsv")), ("fr", include_str!("fr.tsv")), ("id", include_str!("id.tsv")), + ("ko", include_str!("ko.tsv")), ] { let (entries, errors) = catalog::parse_entries(text); assert!(errors.is_empty(), "{code}: {errors:?}"); @@ -1094,6 +1110,32 @@ mod tests { set_current(Language::En); } + /// Korean covers the whole interface (the Spanish catalog's entries and the native macOS + /// Settings menu); like French, the tests require the core menus, and every entry must still + /// have a Spanish counterpart so a renamed or removed source can't leave a dead Korean entry. + #[test] + fn korean_covers_the_menus_and_has_no_dead_entries() { + let (entries, errors) = catalog::parse_entries(include_str!("ko.tsv")); + assert!(errors.is_empty(), "{errors:?}"); + for (ctx, en, _) in &entries { + assert!(ctx.is_empty() && (Language::Es.has(en) || en == "Settings"), "Korean entry without a Spanish one: {ctx:?} {en:?}"); + } + for en in core_menu_labels() { + assert!(Language::Ko.has(&en), "missing Korean menu label: {en}"); + } + for en in crate::menus::MENUS.into_iter().chain(["Audio", "Settings"]) { + assert!(Language::Ko.has(en), "untranslated Korean menu: {en}"); + } + for (en, ko) in [("File", "파일"), ("Edit", "편집"), ("Sequence", "시퀀스"), ("Settings", "설정")] { + assert_eq!(Language::Ko.tr(en), ko); + } + assert_eq!(Language::Ko.name(), "한국어"); + assert_eq!(Language::parse("ko"), Some(Language::Ko)); + for tag in ["ko", "ko-KR", "ko_KR.UTF-8"] { + assert_eq!(Language::from_locales(&[tag.to_string()]), Language::Ko, "{tag}"); + } + } + #[test] fn russian_entries_cover_the_original_menu_catalog() { let app = crate::FilmcraftApp::new(filmcraft_engine::Session::default()); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 04d79ac3..fedd521b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -36,6 +36,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ uic!("app.language.russian", "Русский", ["Edit", "Language"], None), uic!("app.language.french", "Français", ["Edit", "Language"], None), uic!("app.language.indonesian", "Bahasa Indonesia", ["Edit", "Language"], None), + uic!("app.language.korean", "한국어", ["Edit", "Language"], None), uic!("source.playback.toggle", "Source Play/Stop", [], None), uic!("source.playback.play", "Play Source", [], None), uic!("source.playback.stop", "Stop Source", [], None), @@ -195,6 +196,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: | "app.language.russian" | "app.language.french" | "app.language.indonesian" + | "app.language.korean" ) { // Japanese needs the craft-fonts (built with CRAFT_FONTS_DIR) or a font installed on the system if id == "app.language.japanese" && !crate::i18n::install_japanese_font(ctx) { @@ -204,6 +206,10 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: if id == "app.language.chinese" && !crate::i18n::chinese_font_available() { return Err("no Chinese font is installed on this system (for example Noto Sans CJK SC or Microsoft YaHei); the interface stays in English".into()); } + // Korean needs an installed Hangul font (none is bundled; see cjk.rs) + if id == "app.language.korean" && !crate::i18n::korean_font_available() { + return Err("no Korean font is installed on this system (for example Malgun Gothic or Noto Sans CJK KR); the interface stays in English".into()); + } let language = match id { "app.language.japanese" => crate::i18n::Language::Ja, "app.language.spanish" => crate::i18n::Language::Es, @@ -214,6 +220,7 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: "app.language.russian" => crate::i18n::Language::Ru, "app.language.french" => crate::i18n::Language::Fr, "app.language.indonesian" => crate::i18n::Language::Id, + "app.language.korean" => crate::i18n::Language::Ko, _ => crate::i18n::Language::En, }; // The preference is updated in memory before it is written, so a failed write (read-only @@ -674,6 +681,7 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec { "app.language.russian" => it.checked = Some(app.ui.language == crate::i18n::Language::Ru), "app.language.french" => it.checked = Some(app.ui.language == crate::i18n::Language::Fr), "app.language.indonesian" => it.checked = Some(app.ui.language == crate::i18n::Language::Id), + "app.language.korean" => it.checked = Some(app.ui.language == crate::i18n::Language::Ko), _ => {} } if it.id.starts_with("view.") { diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 00f17d35..4cfba134 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (German ships the menus, #630) · **Target:** Adobe Premiere Pro 2026 (26.5.2) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Korean covers the whole interface, #508) · **Target:** Adobe Premiere Pro 2026 (26.5.2) Per-language status of FilmCraft's interface. Premiere 26.5.2 ships 10 interface languages (the installed bundle's `.lproj` folders): German, English, Spanish, French, Italian, Japanese, Korean, @@ -51,7 +51,7 @@ whole interface, one PR) at ~4–8 h per language for the catalog plus tests. | Indonesian | id | 3,541 (same entries as Spanish, plus the macOS Settings item); must cover the core menus, no entry without a Spanish one | as Spanish | Latin (bundled fonts); System Language also maps the legacy `in` code | not recorded; welcome | partial (#867) | review | | Japanese | ja | 3,487 (99.7%) | as zh-cn; system-font kanji may pick the Chinese fallback (#547) | CJK fonts, vertical title text | not recorded | partial | 4–8 h + review | | German | de | 371 (10.6%) | menus only | Latin | yes (#630) | menus only | 5–9 h + review | -| Korean | ko | 0 (0%) | — | needs Hangul fonts in craft-fonts and IME testing | — | none (requested #508) | 10–20 h + review | +| Korean | ko | 3,573 (same entries as Spanish, plus the macOS Settings item) | as Spanish | installed system Hangul font (Malgun Gothic, Apple SD Gothic Neo, Noto Sans CJK KR…); refused without one | not recorded; welcome | partial (#508) | review; Hangul in craft-fonts for the web | | Vietnamese | vi | 0 (0%) | — | Latin with stacked diacritics (font coverage to check) | — | none | 6–12 h + review | | *Other shipped:* Ukrainian | uk | 351 (10.0%) | menus only | Cyrillic (bundled) | not recorded | menus only | 5–9 h | From 154bc148e34ea74ff253e131ba8a78d192fe9cc9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:20:04 -0400 Subject: [PATCH 11/18] #1258: lands.first() instead of lands[0] (a transition with no endpoints can't panic) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/timeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 2a311f91..9408610b 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1903,7 +1903,7 @@ fn move_transition_preview(app: &FilmcraftApp, seq: &Sequence, trn: &filmcraft_p if lands.len() != endpoints.len() { return None; } - let (dest, delta) = lands[0]; + let &(dest, delta) = lands.first()?; lands.iter().skip(1).all(|l| *l == (dest, delta)).then(|| (trn.start + delta, trn.end() + delta, dest)) } From dd92e46583ca9f465df7994223c3709987ee82aa Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:20:20 -0400 Subject: [PATCH 12/18] #1233: drop the stale Hardware decoding row (main already has the current one) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/i18n/es.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b6d114b8..70907f00 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1829,7 +1829,6 @@ Hard Limiter Limitador duro Hard Mix Mezcla definida Hardware decoding Descodificación por hardware - Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. Descodificación por hardware: Automático usa el descodificador de video del sistema (VideoToolbox en macOS, Media Foundation en Windows, VA-API para H.264 y HEVC en Linux) con los flujos que admite, y el descodificador propio de FilmCraft para todo lo demás o si falla el hardware. Los medios ya abiertos conservan su descodificador hasta que se vuelven a abrir. Hardware decoding: Auto uses the system's video decoder (VideoToolbox on macOS, Media Foundation on Windows, VA-API or NVDEC for H.264 and HEVC on Linux) for the streams it supports, and FilmCraft's own decoder for everything else or if the hardware fails. Media that is already open keeps its decoder until it is reopened. Descodificación por hardware: Automático usa el descodificador de video del sistema (VideoToolbox en macOS, Media Foundation en Windows, VA-API o NVDEC para H.264 y HEVC en Linux) para los flujos que admite, y el descodificador propio de FilmCraft para todo lo demás o si falla el hardware. Los medios ya abiertos mantienen su descodificador hasta que se vuelven a abrir. Head Size Tamaño de la cabeza Heart Corazón From 6c3a7ed4ef12469295c3fe6dbe9e607499711e77 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:21:53 -0400 Subject: [PATCH 13/18] #1288: drop the committed .pyc; ignore __pycache__ Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + .../__pycache__/test_install.cpython-313.pyc | Bin 11481 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 packaging/linux/__pycache__/test_install.cpython-313.pyc diff --git a/.gitignore b/.gitignore index 81d7e8b7..d69251bc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ plan/ /log .DS_Store .claude/worktrees/ +__pycache__/ diff --git a/packaging/linux/__pycache__/test_install.cpython-313.pyc b/packaging/linux/__pycache__/test_install.cpython-313.pyc deleted file mode 100644 index 7b771ec8f391c4513cc04cf9f9ad2c17eb19c6f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11481 zcmcgydu$uWnO~Aia!D;EQq+qQWizrYi*hL1mi(6dlx!)nWSc7{PN;Z8kt>N2MY6L? z$7Y)}ZLfE-ae%D5rl#8fR)YeO(cr2g0qOz;n%)J7fy*7Zp%Myf9dJeOA0KdlAH6Fs z|J;4E%Oxdh?KC~$M$qhVcAnqNe81oK%|5o-tOSIG-+cPo=S>9hd;CKU#$w{}zeD0} zf*}yW=otO;I;4ZA;d%XY24wIkc_bp`cSdBCpC)9IpJrs1pA@3xrv+KY4Q<4@b+1lo zJ|p~bfzgB>*nFr6@kNzc2CCQ zvk?@Y6e8=#SXzYZkoz_nkg15Vs{zr{B*xwL}jqQW@iTfxizzCL+a^ zm76m`jMw7vE{NYILUJaK0>$o18IXWAh5dC<7bN1Pw?QIcUSCE@LoT^FH$ZL9Qzz&l zLRIS9C)B3Z_vNQr8%9(b1=OxCmrzrT!3XU_xzZS*oW^1>BrX?ls`biG#>kis>O9bf zW<7JUefhlNT2M}7Q5ov1+7C6TeQKSrHX_ulzGo~-pPJ*5->I7hr28kg8gi@cR}+l2 zM_=sYiF45^U+$NwauP#usuQqlG{-5+JE)VTvQ_Q5{A6f#jXlIj2SHR3pwVp5WPDJCJWB^?>c3UTzQ(?p<1YR=o(~y$ra4Hp#MP#kSe`zhY z)y+?TNp(j#env>9ZtEpWske-+l=Ma7vCPqJ&-kHfKR<0vp;$tg^z7Kqk0*9`+S+%u zL3FIma|9lppzspmSx(OO`s3krA~MZI+pJzg#uZIQ{H0d?XNFG>jxvM3k>RmSZ7h;Z z@cwCd#FNM`fHKK6%*JLpe<~cA2~TmnoY#GMHty9Ue2^uIyBy;MHaU}_KoUY4aaONG zq6BU>1$}T*?HHtzD2%QQ6m{?wiNtRuYd9J`8Rx=@bV@Qulmx;`N+R*lB@!i*f@CTR z)Hpi>b3#}-NG5(dEyUsy6-lP9$i0w5gMmTGbo|^oc6dOtzK&u7#|p42utf3#cu9%m zx%ec)SSFDv(EO4K5M_!Jc&x%a5Tv+&Itg<{X|1q{R%oHyCg- zqn!WsB$^Qr2ZD*m66wo6EG(aJB?TFZ$YO~p<&`WsR$cJa71$A+5KKw6>os?`oQJg% ze-`xjTf|zOyHMxy%42G~b9V#By!DVnZ)vRIOTU^DS3fuBSzdyFhIdsclOGIsDwcTJO3M zS_m!f7wfyAq!pKJzS?{}AzIoC)J~DwxeO)Yd2o%Yp0Bx9^A4H)me{m&Y3%2r+o3ys zqVMzyHB@}JM%#ZddHq86LeAMC(j80W&#kwu%NNDYlb<%+rE=#kclGvol}NU60JT>SK`WMqI=wff42L z4)=8=s@#{ktwLqwZltG=UH_u~jSwa%EBtb9+_#+wPL1R1N zHH}K-M3|pO81BaIW10oK*${a>Dpg0qDSYJEWLik21$?kz%bNw*gplJkNmf2RkwVD` z$Mcc_r4y2Y7ZAe8A{l1GDdd9GZ9Qtk86=zrp2vvq@u2NE$6Va4Qr(6ZM1JrlBvYz5;A;Dla_&jdv5g4kx8xkb6Z>uN( zKZB$u)3HFl2D4E)H;mj0`1u_ew-MUaU^<}^)(1+gq1<~L0dlR9S%DG9>q=|b1L}_6 zFkh{?Y-lskQtnSpsaC)fWW8=gPphqt2iU~-6dZqoY<0!eYgPL%KT9Jqlv~r5BuI*W z60=cT*&rE>)qpl^U#kr*>FNVdnFs9ZBA@rbjvk2@<)UM%<@NQbGY39cqwv9m3Ff;^ z%`ZP0dr$|Z96g4h>1jQd`wO+JwFAKJDg#&?wW@ruF2EuD4D31e2NCE}a@WVWR>-I3 z1WjrSem$ISCuVt5@aL@?&DhdBn2VqYr%-s7&v;}m=3h_6{7En|kxIeF5LU_=YdV3r zco=A%)vK3`lQG1Dc1@?EI4iSNY2}G*>PyP!Q}`jmVn_I_A`AzaCZ(nZ*>Wpyv?Yl|Iare3!!U7#=^cS0hlu$c(p9Mgauk zCJ;W4$b%z{5ojMoQdO7-=?R11NQdK+6>$JKiAXZa0SyzP96~_kxIDxQLA{k!?3ObE;;W6yS(H@Ar zW*Mgt*1(ccUN6afi9u{6d^#>D^v+zIF^{1gGdlmff`x zYgy`8-g~?IPOsQ?^yAu38h_iE_YAMtpMx?5yGOKpR_j}`+``4hDA2<~-9E8yU%u|Z zo!;LZ{?*~T_2S{t-1(q*I9NCo77vBx=3czGLbEW?`LkEgW?#s491~lQ72Lz3dpPfYeuX}>?$v+cPNE4$he|bGBWzWlh;j z+38%zp>ik-jW4`ZNU#`htsM;k~?OLN7=0~rM=G;9WGItt3 z7|*qwxZ9qm&pt2^^lq88NQS9cGSdx_C&qgc92W%}P)K^X^`5KZcJvg@Fr-a&J_RlD zl~jiaU|LiW82vgDfsJJi!UNPui>?|VUxU(=Nd&Z3(Hs7$xKCu$TZ7c7kO!nk$0+2X zT0{oayP2UvC8!LNs)ApaS0Y$`lCdyWrs|+U?p224gfUdF4xX{8 zwSg32GG_`JpXT&bktci@2$(T|iE>3yXZbW5vTmeBm{muy?oD|Pdf7z^`@ln-Z7w@p zv|Otoo%PO&nG(s-TKoUE@s|N#ahvq0n5+0|+rE)bg8pptv`3Tico=zJ6=IQ@S3To= z91I*!hi5j4I0O<0*D6FA$LtjkMzgY07WLtm81&FNh$NG8rpE4Lzy_ZLxQhsc^=*d6 zZW7h;aX*T)Y&ffgj#J;P`@7S>%lySruU$bvjCSYhz!;;eE-mvhKa6GX)#6`-ngZfy(6p z6N+*LV_E(y{~m#Tj-Aj`#52URgo*}HSd&RGEVjZMFfHLitKYDdnACZz;X+APT%d!n z{*ox!a)@2x)C`^9tIJq?zI0+Hk$gQ-%+476e!rTg{K9O?-`(w3nlOPD!G;Dk5svob zQ!b~rmr_=of^IPo$@&ts3?|A7cgKh&nZf@v2CYcd3c(&tDm-3h_KKS8g>oCbK3zqI zD_aU>CBjaIV{vwZn*5L@HrvlEMhTDNL0*fqhIhbWJ zY^di@bIZ@5PRlK&&QsM3_zZ#*G~?KS-uN~|ssa>)eJJ0itfLL?;&qICZ5&C?rsAAX zx75}FxodBGkSqJ?bj7&KbLpXH|BGvdk6^?^>_#)(Z4P`4-Msgl6Y01FcQ9L|q zudH%?!A6sS)QM!t_LO?HPR>`=nt-l&9wIn4iYq_Y*diJm z+!6aiYml)YuG7MnQ zl|i8eU?$-i$w5m2fT>{#m5zPcXk#`2K2ycejLmP{MTG37)@s#M`8lX7w*@MxIG1&K z<&Pn%yY++cg$w-EF!qB+*&A73Zb?mn4^qQ_%{U-m!&42prJHrN1%kkC#In&FDtS@C z^uyf0B9y(A>i838*ytK#3*jv$hhS+{7-xBVhw#pogZ4KuK{MmP=S=NKPb8?&j$##p zscR;JQ~-8OWl)u%g4&p`(TA&N0o@u6KYcdEW(Nqw5YuXuuWDG z)G@BxQ4O*O9J~A~f?Bpbj!MGzG@>@8(zM1=)DA66W?*MrnCBEnuk4!5Y%5=_bCKz! zXU8e9JWt{;fxrfpZ?<<>WB&otkVyFENI=*lodFIeUyQ>Ieunn>_?cMBH<6CTqnPRe zRhkXM-M;}Yn!$l=KY2WG`YbyJ@!%LcFdUG`fnjD0!4Z{j^ti+&goTK@afAh?Siw|R zH#E>amAQ`(%O*~_$4lT!xrZO(=E~qAlD6IAf2r8(%M7(VT zq6>4C$*^v}0m!aCQ{|5--y6ZaM}fZLa|WLRB%(b2CJW}O@7^WpW;gIkOWzci;4Y`o z5h<`)Cl?olEq>(%KH5keF4%b%JZX`%fWPlj{kGtz?~5lR0Le$XOKpA|M!@64bRRT& z&dq+-*|)g+6Vq=^pVa)eCU@?Y!m*3D&fh!_+5gRBZTH=6dvv!gdvvk?0inuQt6rDF z1xh|}>>!^_Ml0-ogfC#x35X;!tXK>s6J@7?>=TEpSA{%~ao8skITcS%NQO9ch$zW``mVCAcS<&HnMWpD*%XA4K7)Cw0Z|TiD-@H4R zqXVn77p_R_QyqnE8v2(*^ulGy0F5bMj#EGpv%fS`;aQVfH zh1OkS>#k*2zV&dS6?@e4ttUPnyL(0)yigd3iUZO7z~l-&^+z*Ny`y03_`=rlC2xaO z`1rsH^0d+LiP3u6Y4(z3BU1k624QR)#b5TYEdDx&WhE=i!fs8&xnw1pW%2b;@l6d2 zS37W5hW%iPB+H@>sH~WrqmYuQbRs6;%?AE5P?*6)9KKJGZ+_4)E_o40lQ@dwD2by5 zh@^U!g>OH>XUK*H0mUZL&{L{osyXasvm(VD+&Tz=<(PD!n{wJ;4Yl{K8~INWSKgI~((26GUJ8UvA?1k_R{`L{Ta#66=Q;euFsD+ihC zQx@h3dKdEGa1ne0q6Y?@PWL+^^n1ecFGSNl!gh~v+#{;*5jFRSDu}_~MbUFZ-+QxQ zZWYa~dGpqSxkEH}yhW~Bw!LNggDI)gP3x{7C^*|iXZs%r`R9YEfgtI*Oy1yHCF#E! Q`R>T~@%0GVERW}Z0rr_hZU6uP From 62aed1acc209ecc983508b85ec5196fc62ae9ddd Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:29:19 -0400 Subject: [PATCH 14/18] #1303: frame times in i128 via Tick::from_rational (a hostile F1:i64::MAX rate can't overflow) Co-Authored-By: Claude Opus 5.5 --- crates/codecs/src/y4m.rs | 4 ++-- crates/codecs/tests/y4m_file.rs | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/codecs/src/y4m.rs b/crates/codecs/src/y4m.rs index 912cd5aa..76937e17 100644 --- a/crates/codecs/src/y4m.rs +++ b/crates/codecs/src/y4m.rs @@ -180,7 +180,7 @@ impl Y4mSource { return Err(bad("no complete frames")); } let color = ColorInfo { matrix: filmcraft_frame::default_matrix(w, h), range: if full { Range::Full } else { Range::Limited }, ..ColorInfo::REC709 }; - let duration = Tick::from_rational(frames.len() as i64 * rate.den, 1, rate.num); + let duration = Tick::from_rational(frames.len() as i64, rate.den, rate.num); let bitrate = (frame_bytes as u64 * 8).checked_mul(rate.num as u64).map(|b| b / rate.den as u64); let info = MediaInfo { name: name.to_string(), @@ -244,7 +244,7 @@ impl Y4mSource { data: pixels, color: v.color, par: v.par, - pts: Tick::from_rational(idx as i64 * self.rate.den, 1, self.rate.num), + pts: Tick::from_rational(idx as i64, self.rate.den, self.rate.num), }) } } diff --git a/crates/codecs/tests/y4m_file.rs b/crates/codecs/tests/y4m_file.rs index 31623494..f9aa1e86 100644 --- a/crates/codecs/tests/y4m_file.rs +++ b/crates/codecs/tests/y4m_file.rs @@ -99,9 +99,17 @@ fn damaged_and_hostile_y4m_files_fail_without_panicking() { b[i] ^= 0x41; cases.push(b); } - for header in - ["W99999999 H99999999 F25:1", "W0 H4 F25:1", "W6 H4 F0:0", "W6 H4 F25:0", "W6 H4 F25:1 C411", "W6 H4 F25:1 Cbogus", "W6 H4 A0:0 F-5:1", "H4 F25:1"] - { + for header in [ + "W99999999 H99999999 F25:1", + "W0 H4 F25:1", + "W6 H4 F0:0", + "W6 H4 F25:0", + "W6 H4 F25:1 C411", + "W6 H4 F25:1 Cbogus", + "W6 H4 A0:0 F-5:1", + "H4 F25:1", + "W6 H4 F1:9223372036854775807", + ] { cases.push(y4m(header, "FRAME", &[vec![0; 36]])); } for (i, b) in cases.into_iter().enumerate() { From 39f60de2f4ca6d73c788ee49925bb1c90a44cf55 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:34:27 -0400 Subject: [PATCH 15/18] #1457: bound slice-group map type 6 by the picture's map units (CIF FMO is 396), drop pr1441.md Co-Authored-By: Claude Opus 5.5 --- crates/h264/src/params.rs | 3 ++- pr1441.md | 18 ------------------ 2 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 pr1441.md diff --git a/crates/h264/src/params.rs b/crates/h264/src/params.rs index dd7b4d23..5b92a605 100644 --- a/crates/h264/src/params.rs +++ b/crates/h264/src/params.rs @@ -594,7 +594,8 @@ impl Pps { } 6 => { let n_minus1 = r.read_ue()?; - ensure!(n_minus1 <= 128, "slice_group_run_length out of range"); + // pic_size_in_map_units_minus1: one slice_group_id per map unit of the picture. + ensure!(n_minus1 < sps.pic_width_in_mbs.saturating_mul(sps.pic_height_in_map_units), "pic_size_in_map_units out of range"); let n = n_minus1 + 1; let bits = 32 - (num_slice_groups - 1).leading_zeros(); for _ in 0..n { diff --git a/pr1441.md b/pr1441.md deleted file mode 100644 index f3e0ea34..00000000 --- a/pr1441.md +++ /dev/null @@ -1,18 +0,0 @@ -## What - -`Pps::parse` added offsets to input-derived fields before validating them, so six malformed-PPS fields panic (overflow checks on) instead of returning `Error::Invalid`. The panic reaches the public `Decoder::decode` API. Fixes #1441. - -## Changes (`crates/h264/src/params.rs`) - -- **Validate before incrementing**: `num_slice_groups_minus1 < 8`, `num_ref_idx_l0/l1_default_active_minus1 < 32` are checked pre-`+1`; the map-type-6 run-length count is bounded before `+1`. -- **QP/QS range checks per 7.4.2.2**: `pic_init_qp_minus26` must be in `-(26 + QpBdOffsetY)..=25` (bit-depth aware via the SPS) and `pic_init_qs_minus26` in `-26..=25`, checked before adding 26. -- **`weighted_bipred_idc`**: concealed `3 → 2` per the 7.4.2.2.1 fall-back rule ("shall be considered equal to 2") rather than rejected — matches the spec text, and the slice decoder already only distinguishes 0/1/2. - -## Tests (`crates/h264/tests/pps_validation.rs`, 6 tests) - -- The reporter's literal 12-byte SPS + 18-byte PPS: `catch_unwind` now sees `Err`, not a panic. -- Synthetic bit-built PPSes for hostile `num_slice_groups`, hostile `num_ref_idx_l1`, hostile `pic_init_qp`. -- `weighted_bipred_idc = 3` parses with the field concealed to 2. -- A well-formed minimal PPS still parses (control). - -Red/green verified: reporter-literal and concealment tests fail on main (panic at params.rs:604 / silent accept), all 6 pass with the fix; negative control via stash round-trip. Full `filmcraft-h264` suite green (7 test binaries, 0 failures), `cargo fmt --check` and `cargo clippy -p filmcraft-h264 --all-targets` clean. From e6973f59579a9a1fc0003aed1c3ed4d60ba87d32 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 01:42:53 -0400 Subject: [PATCH 16/18] #1427: ripple sequence markers by the removed head (they didn't move); params name clips/clip Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands.rs | 8 ++++---- crates/engine/src/ripple_head_tests.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 72088889..c99e5c64 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -1677,7 +1677,7 @@ fn build() -> Vec { })?; Ok(Value::Null) }), - cmd!("edit.rippleDeleteHead", "Ripple Delete Head Frame", ["Edit"], None, r#"{"clip":id}?"#, has_selection, |s, p| { + cmd!("edit.rippleDeleteHead", "Ripple Delete Head Frame", ["Edit"], None, r#"{"clips":[id]?,"clip":id?}"#, has_selection, |s, p| { // #1357: remove the head — from each selected clip's in point through the playhead — // and ripple the sequence closed. Linked clips ripple as one group (the engine's // group trim moves every member's in point by one delta and shifts the synced @@ -1718,11 +1718,11 @@ fn build() -> Vec { } continue; } - let before = live[0].1; + let start = live.iter().map(|(_, s, _)| *s).min().unwrap_or(ph); edit::ripple_trim_group(q, &ids, Edge::In, head, ctx)?; if st.ripple_sequence_markers { - let after = q.find_item(live[0].0).map(|(_, i)| i.start).unwrap_or(before); - crate::sequence_tools::ripple_markers(&mut q.markers, after, -(after - before)); + // the head [start, start + head) closes: later markers move back by it + crate::sequence_tools::ripple_markers(&mut q.markers, start + head, Tick::ZERO - head); } } Ok(()) diff --git a/crates/engine/src/ripple_head_tests.rs b/crates/engine/src/ripple_head_tests.rs index 7661d206..61588539 100644 --- a/crates/engine/src/ripple_head_tests.rs +++ b/crates/engine/src/ripple_head_tests.rs @@ -86,3 +86,23 @@ fn ripple_delete_head_past_the_end_removes_the_whole_clip() { assert_eq!(range(&s, pic3), (f(&s, 48), f(&s, 96)), "later clips closed the whole 48-frame gap"); assert_eq!(range(&s, snd3), (f(&s, 48), f(&s, 96))); } + +/// With Ripple Sequence Markers on, markers after the removed head move back with the clips, and +/// markers inside the head go with it. +#[test] +fn ripple_delete_head_ripples_sequence_markers() { + let mut s = Session::default(); + let [(_, _), (pic2, _), (_, _), (_, _)] = four_clips(&mut s); + s.state.ripple_sequence_markers = true; + let at = |s: &Session, n: i64| s.sequence_rate().tick_of(n).0; + for (frame, name) in [(30, "before"), (52, "inside"), (120, "after")] { + s.execute("markers.add", json!({"time": at(&s, frame), "name": name})).unwrap(); + } + s.set_playhead(f(&s, 60)); + s.state.selection = vec![pic2]; + + s.execute("edit.rippleDeleteHead", json!({})).unwrap(); + let q = s.active_sequence().unwrap(); + let markers: Vec<(String, Tick)> = q.markers.iter().map(|m| (m.name.clone(), m.start)).collect(); + assert_eq!(markers, vec![("before".to_string(), f(&s, 30)), ("after".to_string(), f(&s, 108))], "the 12-frame head closes"); +} From c3f931d0c4f68f84017f665ee02380b0c7be2267 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 02:04:44 -0400 Subject: [PATCH 17/18] #1502: English fallback without a Hangul font (as for Chinese); doc comment back on available(); drop the old Korean ROADMAP row; Korean rows for strings added on main since Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 - crates/ui-egui/src/cjk.rs | 4 ++-- crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 14 ++++++++++++++ crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/zh-cn.tsv | 1 + crates/ui-egui/src/lib.rs | 10 ++++++++-- 10 files changed, 30 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 7eec5268..9966cea5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -87,7 +87,6 @@ Premiere ships 10 interface languages. Measured from our catalogs (3,498 strings | Korean | partial (whole interface; needs an installed Hangul font; native review welcome) | ~100% | | Japanese | partial (whole interface) | 99.7% | | German | menus only (native-reviewed) | 10.6% | -| Korean | none | 0% | | Vietnamese | none | 0% | Also shipped: Ukrainian (menus only, 10%). Detail: [docs/localization-parity.md](docs/localization-parity.md). diff --git a/crates/ui-egui/src/cjk.rs b/crates/ui-egui/src/cjk.rs index ab1345b2..bf089a2f 100644 --- a/crates/ui-egui/src/cjk.rs +++ b/crates/ui-egui/src/cjk.rs @@ -53,14 +53,14 @@ fn find_system_font(sample: &str, preferred: &[&str]) -> Option> { Some(Arc::new(FontData { font: std::borrow::Cow::Owned(face.data()?), index: face.info.index, tweak: Default::default() })) } -/// Whether [`install`] has a Chinese face to add: craft-fonts Hans/Hant faces, or an installed -/// system face (scanned once per process). The Simplified Chinese interface needs one. /// Whether [`install`] has a Hangul face to add (an installed system face; none is bundled). /// The Korean interface needs one. pub(crate) fn korean_available() -> bool { korean_font().is_some() } +/// Whether [`install`] has a Chinese face to add: craft-fonts Hans/Hant faces, or an installed +/// system face (scanned once per process). The Simplified Chinese interface needs one. pub(crate) fn available() -> bool { craft_fonts().next().is_some() || system_font().is_some() } diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 32f496a9..fb7f245e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3438,6 +3438,7 @@ in entrada The language changed but could not be saved: {e} El idioma ha cambiado, pero no se ha podido guardar: {e} no Chinese font is installed on this system; the interface stays in English no hay ninguna fuente china instalada en este sistema; la interfaz sigue en inglés + no Korean font is installed on this system; the interface stays in English No hay ninguna fuente coreana instalada en este sistema; la interfaz sigue en inglés no Japanese font is installed on this system; the interface stays in English no hay ninguna fuente japonesa instalada en este sistema; la interfaz sigue en inglés Show Mostrar Sort Ordenar diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 845d0148..2870df59 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3420,6 +3420,7 @@ in entrée The language changed but could not be saved: {e} La langue a été changée mais n'a pas pu être enregistrée : {e} no Chinese font is installed on this system; the interface stays in English aucune police chinoise n'est installée sur ce système ; l'interface reste en anglais + no Korean font is installed on this system; the interface stays in English aucune police coréenne n'est installée sur ce système ; l'interface reste en anglais no Japanese font is installed on this system; the interface stays in English aucune police japonaise n'est installée sur ce système ; l'interface reste en anglais Show Afficher Sort Trier diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index dd220050..6ffd05e2 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3437,6 +3437,7 @@ in masuk The language changed but could not be saved: {e} Bahasa telah diubah tetapi tidak dapat disimpan: {e} no Chinese font is installed on this system; the interface stays in English tidak ada font Tionghoa yang terpasang di sistem ini; antarmuka tetap dalam bahasa Inggris + no Korean font is installed on this system; the interface stays in English tidak ada font Korea yang terpasang di sistem ini; antarmuka tetap dalam bahasa Inggris no Japanese font is installed on this system; the interface stays in English tidak ada font Jepang yang terpasang di sistem ini; antarmuka tetap dalam bahasa Inggris Show Tampilkan Sort Urutkan diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 789c686e..a6f6d134 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3426,6 +3426,7 @@ in イン The language changed but could not be saved: {e} 表示言語は変更されましたが、保存できませんでした: {e} no Chinese font is installed on this system; the interface stays in English このシステムには中国語フォントがインストールされていないため、表示は英語のままです + no Korean font is installed on this system; the interface stays in English このシステムには韓国語フォントがインストールされていないため、表示は英語のままです no Japanese font is installed on this system; the interface stays in English このシステムには日本語フォントがインストールされていないため、表示は英語のままです Show 表示 Sort 並べ替え diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index d88aa57a..693557be 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2044,6 +2044,7 @@ No candidates yet — Search a folder or Locate the file. 후보 없음 — 폴더를 검색하거나 파일을 찾으세요. No captions on this track. Press + to add one at the playhead. 이 트랙에 자막 없음. +를 눌러 재생 헤드에 추가합니다. no Chinese font is installed on this system; the interface stays in English 중국어 글꼴이 설치되어 있지 않아 인터페이스가 영어로 유지됩니다 + no Korean font is installed on this system; the interface stays in English 한국어 글꼴이 설치되어 있지 않아 인터페이스가 영어로 유지됩니다 No contributor data was built into this copy. 이 사본에 기여자 데이터가 포함되지 않았습니다. No effects 효과 없음 No events. 이벤트 없음 @@ -3581,3 +3582,16 @@ ✔ matches ✔ 일치 ✔ same file ✔ 같은 파일 ✖ different file ✖ 다른 파일 + Open Recent 최근 항목 열기 + Clear Recent Files 최근 파일 목록 지우기 + Recent 최근 항목 + 32 bit 32비트 + 32 bit float 32비트 부동 소수점 + FLAC stores whole-number samples only FLAC은 정수 샘플만 저장합니다 + MXF export writes integer PCM only MXF 내보내기는 정수 PCM만 기록합니다 + Opens in your web browser: {url} 웹 브라우저에서 엽니다: {url} + Saves this session's log and shows it in your file manager. 이 세션의 로그를 저장하고 파일 관리자에서 보여 줍니다. + Shows a report on this computer. 이 컴퓨터에 대한 보고서를 보여 줍니다. + Shows the version, community links and credits in a FilmCraft window. FilmCraft 창에 버전, 커뮤니티 링크, 제작진을 보여 줍니다. + Couldn't open your web browser 웹 브라우저를 열 수 없습니다 + Open this address in your browser yourself: 이 주소를 브라우저에서 직접 여세요: diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e7e14743..63882a67 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3434,6 +3434,7 @@ in entrada The language changed but could not be saved: {e} O idioma mudou, mas não pôde ser salvo: {e} no Chinese font is installed on this system; the interface stays in English nenhuma fonte chinesa está instalada neste sistema; a interface permanece em inglês + no Korean font is installed on this system; the interface stays in English nenhuma fonte coreana está instalada neste sistema; a interface permanece em inglês no Japanese font is installed on this system; the interface stays in English nenhuma fonte japonesa está instalada neste sistema; a interface permanece em inglês Show Mostrar Sort Ordenar diff --git a/crates/ui-egui/src/i18n/zh-cn.tsv b/crates/ui-egui/src/i18n/zh-cn.tsv index 608f5de4..186b6826 100644 --- a/crates/ui-egui/src/i18n/zh-cn.tsv +++ b/crates/ui-egui/src/i18n/zh-cn.tsv @@ -3420,6 +3420,7 @@ in 入 The language changed but could not be saved: {e} 语言已更改但无法保存:{e} no Chinese font is installed on this system; the interface stays in English 此系统未安装中文字体;界面保持英文 + no Korean font is installed on this system; the interface stays in English 此系统未安装韩文字体;界面保持英文 no Japanese font is installed on this system; the interface stays in English 此系统未安装日语字体;界面保持英文 Show 显示 Sort 排序 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f25d796c..cb4a2344 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -720,6 +720,9 @@ impl FilmcraftApp { } else if language == i18n::Language::ZhCn && !i18n::chinese_font_available() { self.ui.language = i18n::Language::En; self.ui.status = tl!("no Chinese font is installed on this system; the interface stays in English").into(); + } else if language == i18n::Language::Ko && !i18n::korean_font_available() { + self.ui.language = i18n::Language::En; + self.ui.status = tl!("no Korean font is installed on this system; the interface stays in English").into(); } else { self.ui.language = language; } @@ -2042,9 +2045,12 @@ impl eframe::App for FilmcraftApp { theme::install(ctx, &self.tokens); // theme::install replaces the fonts: add the system Japanese font back (or fall back to // English when a saved Japanese setting meets a system without one) - // (likewise Chinese without a Chinese face) + // (likewise Chinese without a Chinese face, Korean without a Hangul one) let japanese_missing = self.ui.language == i18n::Language::Ja && !i18n::install_japanese_font(ctx); - if japanese_missing || (self.ui.language == i18n::Language::ZhCn && !i18n::chinese_font_available()) { + if japanese_missing + || (self.ui.language == i18n::Language::ZhCn && !i18n::chinese_font_available()) + || (self.ui.language == i18n::Language::Ko && !i18n::korean_font_available()) + { self.ui.language = i18n::Language::En; } self.styled = true; From 7c9da91184a85a79223aa34531facafccc572bd8 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 02:33:47 -0400 Subject: [PATCH 18/18] Media Browser drag test: count the dragged files' clips by name (#1442) The drop lands inside the demo's long Ambient_Score.wav on A2, and overwriting there splits it, so the total clip count went up by 3, not 2, and the test failed deterministically. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/tests/project_panel_ui.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/tests/project_panel_ui.rs b/crates/ui-egui/tests/project_panel_ui.rs index eb6bb3ec..87874210 100644 --- a/crates/ui-egui/tests/project_panel_ui.rs +++ b/crates/ui-egui/tests/project_panel_ui.rs @@ -781,16 +781,18 @@ fn media_browser_drag_places_every_selected_file() { d.exec("mediaBrowser.select", json!({"paths": [a, b]})); d.frames(2); let n0 = d.app().session.project.items.len(); - let clips = |d: &mut Driver| { - d.app().session.active_sequence().map(|q| q.video_tracks.iter().chain(&q.audio_tracks).map(|t| t.items.len()).sum::()).unwrap_or(0) + // clips named after each dragged file (a drop inside a long clip also splits that one) + let placed = |d: &mut Driver, name: &str| { + d.app().session.active_sequence().map_or(0, |q| q.video_tracks.iter().chain(&q.audio_tracks).flat_map(|t| &t.items).filter(|i| i.name == name).count()) }; - let c0 = clips(&mut d); + assert_eq!((placed(&mut d, "a-interview.wav"), placed(&mut d, "b-roll.wav")), (0, 0)); let e = d.rect("mediaBrowser.entry.b-roll.wav"); let tl = d.rect("panel.Timeline"); d.ok("ui.drag", json!({"from": {"x": e[0] + 40.0, "y": e[1] + e[3] / 2.0}, "to": {"x": tl[0] + tl[2] * 0.6, "y": tl[1] + tl[3] * 0.75}, "steps": 20})); d.frames(6); assert_eq!(d.app().session.project.items.len(), n0 + 1, "only the new file is imported"); - assert_eq!(clips(&mut d), c0 + 2, "both files are placed; status: {}", d.app().ui.status); + let status = d.app().ui.status.clone(); + assert_eq!((placed(&mut d, "a-interview.wav"), placed(&mut d, "b-roll.wav")), (1, 1), "both files are placed; status: {status}"); let _ = std::fs::remove_dir_all(&dir); }