diff --git a/crates/engine/src/interchange.rs b/crates/engine/src/interchange.rs
--- a/crates/engine/src/interchange.rs
+++ b/crates/engine/src/interchange.rs
@@ -80,16 +80,37 @@ pub fn import(s: &mut Session, path: &str, bytes: &[u8], format: Format) -> Resu
             sequence.settings.validate().map_err(|e| EngineError::Other(format!("{path}: invalid imported sequence settings: {e}")))?;
         }
     }
-    let before: std::collections::HashSet<ItemId> = s.project.items.keys().copied().collect();
     let name = opts.name.clone().unwrap_or_else(|| format.name().to_string());
-    let seqs = s.edit(&format!("Import {name}"), |proj, _| {
-        let seqs = filmcraft_interchange::merge_into(proj, fragment, None);
+    let infer_size = !fragment.source_size_sequences.is_empty();
+    let apply = |session: &mut Session| import_fragment(session, path, fragment, report, format, &name);
+    if infer_size {
+        // Sizing and its auto-points must undo with the import, never expose a half-sized cut.
+        s.edit_group("Import OTIO", apply)
+    } else {
+        apply(s)
+    }
+}
+
+fn import_fragment(
+    s: &mut Session,
+    path: &str,
+    fragment: filmcraft_interchange::Imported,
+    report: filmcraft_interchange::Report,
+    format: Format,
+    name: &str,
+) -> Result<Value> {
+    let before: std::collections::HashSet<ItemId> = s.project.items.keys().copied().collect();
+    let (seqs, source_size_sequences) = s.edit(&format!("Import {name}"), |proj, _| {
+        let (seqs, source_size_sequences) = filmcraft_interchange::merge_with_provenance(proj, fragment, None);
         // A document that sets only some Motion parameters leaves the others at their "auto"
         // (NaN) defaults. Resolve them as placing a clip does, for every format. A media file's
         // picture size is only what the document says so far (an EDL says nothing: the importer
         // assumes the sequence size), so its clips' anchors wait for Link Media below.
-        proj.resolve_placed_auto_points(|seq| !before.contains(&seq), |source| !is_file_media(source));
-        Ok(seqs)
+        proj.resolve_placed_auto_points(
+            |seq| !before.contains(&seq) && !source_size_sequences.contains(&seq),
+            |source| !is_file_media(source) && !source_size_sequences.contains(&source.id),
+        );
+        Ok((seqs, source_size_sequences))
     })?;
     // Link media: probe each new file-backed item that exists, through the host's reader as a
     // direct import does (only the container index is read now, not the whole file: #461).
@@ -109,6 +130,7 @@ pub fn import(s: &mut Session, path: &str, bytes: &[u8], format: Format) -> Resu
     // The document's media paths are opened as stored paths of the document.
     s.media.add_stored_paths(new_media.iter().map(|(_, p)| p.clone()), Some(path));
     let mut linked = 0;
+    let mut linked_items = std::collections::HashSet::new();
     let mut offline = Vec::new();
     for (id, mpath) in new_media {
         let file = match s.media.follow(&mpath) {
@@ -137,15 +159,37 @@ pub fn import(s: &mut Session, path: &str, bytes: &[u8], format: Format) -> Resu
                         }
                     }
                     // the picture size is now the file's own: centre the anchors of its clips in it
-                    proj.resolve_placed_auto_points(|seq| !before.contains(&seq), |source| source.id == id);
+                    proj.resolve_placed_auto_points(|seq| !before.contains(&seq) && !source_size_sequences.contains(&seq), |source| source.id == id);
                     Ok(())
                 })?;
                 s.media.insert_file(id, &mpath, src);
                 linked += 1;
+                linked_items.insert(id);
             }
             None => offline.push(mpath),
         }
     }
+    if !source_size_sequences.is_empty() {
+        s.edit("Size imported sequences", |proj, _| {
+            let sizes: Vec<_> = source_size_sequences
+                .iter()
+                .filter_map(|&id| first_linked_picture(proj, id, &source_size_sequences, &linked_items).map(|size| (id, size)))
+                .collect();
+            for (id, (width, height)) in sizes {
+                if let Some(sequence) = proj.sequence_mut(id) {
+                    let mut settings = sequence.settings.clone();
+                    settings.width = width;
+                    settings.height = height;
+                    settings.preset = format!("Custom {width}x{height} {}", settings.frame_rate.label());
+                    if settings.validate().is_ok() {
+                        sequence.settings = settings;
+                    }
+                }
+            }
+            proj.resolve_placed_auto_points(|seq| !before.contains(&seq), |_| true);
+            Ok(())
+        })?;
+    }
     if let Some(&first) = seqs.first() {
         s.state.active_sequence = Some(first);
         if !s.state.open_sequences.contains(&first) {
@@ -161,6 +205,46 @@ pub fn import(s: &mut Session, path: &str, bytes: &[u8], format: Format) -> Resu
     }))
 }
 
+// Choose the earliest picture in each video track, visiting tracks in document order.
+// Only successfully linked files provide dimensions; parser placeholders are not evidence.
+fn first_linked_picture(
+    project: &filmcraft_project::Project,
+    root: ItemId,
+    source_size_sequences: &[ItemId],
+    linked: &std::collections::HashSet<ItemId>,
+) -> Option<(u32, u32)> {
+    let mut visited = std::collections::HashSet::new();
+    let mut pending = vec![root];
+    while let Some(id) = pending.pop() {
+        if !visited.insert(id) {
+            continue;
+        }
+        let Some(item) = project.item(id) else { continue };
+        match &item.kind {
+            ItemKind::Media(media) if linked.contains(&id) => {
+                if let Some(video) = &media.info.video
+                    && filmcraft_project::validate_frame_size(video.width, video.height).is_ok()
+                {
+                    return Some((video.width, video.height));
+                }
+            }
+            ItemKind::Subclip { parent, .. } => pending.push(*parent),
+            ItemKind::Sequence(sequence) => {
+                if !source_size_sequences.contains(&id) {
+                    return Some((sequence.settings.width, sequence.settings.height));
+                }
+                for track in sequence.video_tracks.iter().rev() {
+                    let mut clips: Vec<_> = track.items.iter().collect();
+                    clips.sort_by_key(|clip| clip.start);
+                    pending.extend(clips.into_iter().rev().map(|clip| clip.item));
+                }
+            }
+            _ => {}
+        }
+    }
+    None
+}
+
 /// `file.exportInterchange {format: "edl"|"xml"|"fcpxml"|"otio"|"aaf"|"omf" = "xml", path, sequence?}`
 pub fn export(s: &mut Session, p: &Value) -> Result<Value> {
     // FCP7 XML only when no format is asked for: a format we don't know is an error, not XML.
diff --git a/crates/engine/src/interchange_auto_points_tests.rs b/crates/engine/src/interchange_auto_points_tests.rs
--- a/crates/engine/src/interchange_auto_points_tests.rs
+++ b/crates/engine/src/interchange_auto_points_tests.rs
@@ -256,3 +256,145 @@ fn an_interchange_import_links_media_without_reading_the_files_whole() {
     assert_eq!(s.project.source_size(clip.item), Some((320, 180)), "the file's real size, from its index");
     let _ = std::fs::remove_dir_all(&d);
 }
+
+#[test]
+fn otio_without_sequence_settings_uses_the_linked_picture_size() {
+    let d = tmp_dir("otio-source-size");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    let range = json!({"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","value":0,"rate":24},"duration":{"OTIO_SCHEMA":"RationalTime.1","value":24,"rate":24}});
+    let doc = json!({"OTIO_SCHEMA":"Timeline.1","name":"Portrait cut","tracks":{"OTIO_SCHEMA":"Stack.1","children":[{"OTIO_SCHEMA":"Track.1","kind":"Video","children":[{"OTIO_SCHEMA":"Clip.2","name":"Portrait","source_range":range,"media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mov","available_range":range}}]}]}});
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap();
+    let mut s = Session::default();
+    let (result, seq) = import(&mut s, &path);
+    assert_eq!(result["linkedMedia"], 1, "the host actually probed the local source");
+    let settings = &s.project.sequence(seq).unwrap().settings;
+    assert_eq!((settings.width, settings.height), (48, 80));
+    let clip = first_clip(&s, seq);
+    assert_eq!(motion_point(&clip, "position"), Vec2::new(24.0, 40.0), "resolve placement against the inferred canvas");
+    assert_eq!(motion_point(&clip, "anchor"), Vec2::new(24.0, 40.0));
+}
+
+fn portrait_otio(path: &str) -> serde_json::Value {
+    let range = json!({"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","value":0,"rate":24},"duration":{"OTIO_SCHEMA":"RationalTime.1","value":24,"rate":24}});
+    json!({"OTIO_SCHEMA":"Timeline.1","name":"Cut","tracks":{"OTIO_SCHEMA":"Stack.1","children":[{"OTIO_SCHEMA":"Track.1","kind":"Video","children":[{"OTIO_SCHEMA":"Clip.2","source_range":range,"media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":path,"available_range":range}}]}]}})
+}
+
+#[test]
+fn otio_explicit_hd_canvas_overrides_a_linked_portrait() {
+    let d = tmp_dir("otio-explicit-size");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    let mut doc = portrait_otio("portrait.mov");
+    doc["metadata"] = json!({"filmcraft":{"settings":filmcraft_project::SequenceSettings::default()}});
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap();
+    let mut session = Session::default();
+    let (_, id) = import(&mut session, &path);
+    let settings = &session.project.sequence(id).unwrap().settings;
+    assert_eq!((settings.width, settings.height), (1920, 1080));
+    assert_eq!(motion_point(&first_clip(&session, id), "position"), Vec2::new(960., 540.));
+    std::fs::remove_dir_all(d).unwrap();
+}
+
+#[test]
+fn otio_offline_picture_keeps_the_default_canvas() {
+    let d = tmp_dir("otio-offline-size");
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&portrait_otio("absent.mov")).unwrap()).unwrap();
+    let mut session = Session::default();
+    let (result, id) = import(&mut session, &path);
+    assert_eq!(result["linkedMedia"], 0);
+    let settings = &session.project.sequence(id).unwrap().settings;
+    assert_eq!((settings.width, settings.height), (1920, 1080));
+    assert_eq!(motion_point(&first_clip(&session, id), "position"), Vec2::new(960., 540.));
+    std::fs::remove_dir_all(d).unwrap();
+}
+
+#[test]
+fn otio_nested_canvas_uses_explicit_settings_or_the_linked_picture() {
+    let d = tmp_dir("otio-nested-size");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    for explicit in [false, true] {
+        let original = portrait_otio("portrait.mov");
+        let mut nested = original["tracks"].clone();
+        nested["source_range"] = original["tracks"]["children"][0]["children"][0]["source_range"].clone();
+        if explicit {
+            nested["metadata"] = json!({"filmcraft":{"settings":filmcraft_project::SequenceSettings::default()}});
+        }
+        let mut doc = original;
+        doc["tracks"]["children"][0]["children"] = json!([nested]);
+        let path = d.join("cut.otio");
+        std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap();
+        let mut session = Session::default();
+        let (_, id) = import(&mut session, &path);
+        let settings = &session.project.sequence(id).unwrap().settings;
+        let expected = if explicit { (1920, 1080) } else { (48, 80) };
+        assert_eq!((settings.width, settings.height), expected);
+        assert_eq!(motion_point(&first_clip(&session, id), "position"), Vec2::new(expected.0 as f64 / 2., expected.1 as f64 / 2.));
+        assert!(auto_points(&session.project).is_empty());
+    }
+    std::fs::remove_dir_all(d).unwrap();
+}
+
+#[test]
+fn otio_source_sizing_and_auto_points_undo_and_redo_together() {
+    let d = tmp_dir("otio-size-history");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&portrait_otio("portrait.mov")).unwrap()).unwrap();
+    let mut session = Session::default();
+    let before = session.project.clone();
+    let (_, _) = import(&mut session, &path);
+    let imported = session.project.clone();
+    session.execute("edit.undo", json!({})).unwrap();
+    assert_eq!(*session.project, *before);
+    session.execute("edit.redo", json!({})).unwrap();
+    assert_eq!(*session.project, *imported);
+    std::fs::remove_dir_all(d).unwrap();
+}
+
+#[test]
+fn otio_picture_choice_follows_video_timeline_not_media_allocation() {
+    let d = tmp_dir("otio-picture-order");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    make_movie(&d.join("landscape.mov"), DemoScene::OceanSunset, 80, 48, 24);
+    let mut doc = portrait_otio("portrait.mov");
+    let landscape = portrait_otio("landscape.mov");
+    let mut audio = landscape["tracks"]["children"][0].clone();
+    audio["kind"] = json!("Audio");
+    let mut video = doc["tracks"]["children"][0].clone();
+    video["children"].as_array_mut().unwrap().push(landscape["tracks"]["children"][0]["children"][0].clone());
+    // Audio creates landscape's media item first; it must not choose the video canvas.
+    doc["tracks"]["children"] = json!([audio, video]);
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap();
+    let mut session = Session::default();
+    let (result, id) = import(&mut session, &path);
+    assert_eq!(result["linkedMedia"], 2);
+    let settings = &session.project.sequence(id).unwrap().settings;
+    assert_eq!((settings.width, settings.height), (48, 80));
+    assert_eq!(settings.frame_rate, filmcraft_time::FrameRate::new(24, 1));
+    assert_eq!(settings.preset, "Custom 48x80 24");
+    assert!(auto_points(&session.project).is_empty());
+    std::fs::remove_dir_all(d).unwrap();
+}
+
+#[test]
+fn otio_explicit_parent_waits_for_an_inferred_nested_anchor() {
+    let d = tmp_dir("otio-nested-anchor");
+    make_movie(&d.join("portrait.mov"), DemoScene::OceanSunset, 48, 80, 24);
+    let original = portrait_otio("portrait.mov");
+    let mut nested = original["tracks"].clone();
+    nested["source_range"] = original["tracks"]["children"][0]["children"][0]["source_range"].clone();
+    let mut doc = original;
+    doc["metadata"] = json!({"filmcraft":{"settings":filmcraft_project::SequenceSettings::default()}});
+    doc["tracks"]["children"][0]["children"] = json!([nested]);
+    let path = d.join("cut.otio");
+    std::fs::write(&path, serde_json::to_vec(&doc).unwrap()).unwrap();
+    let mut session = Session::default();
+    let (_, id) = import(&mut session, &path);
+    let clip = first_clip(&session, id);
+    assert_eq!(motion_point(&clip, "position"), Vec2::new(960., 540.));
+    assert_eq!(motion_point(&clip, "anchor"), Vec2::new(24., 40.));
+    std::fs::remove_dir_all(d).unwrap();
+}
diff --git a/crates/interchange/src/common.rs b/crates/interchange/src/common.rs
--- a/crates/interchange/src/common.rs
+++ b/crates/interchange/src/common.rs
@@ -348,12 +348,13 @@ pub(crate) struct Builder {
     pub p: Project,
     media: HashMap<String, ItemId>,
     pub top: Vec<ItemId>,
+    pub source_size_sequences: Vec<ItemId>,
     next_link: u64,
 }
 
 impl Builder {
     pub fn new(name: &str) -> Self {
-        Self { p: Project::new(name), media: HashMap::new(), top: Vec::new(), next_link: 1 }
+        Self { p: Project::new(name), media: HashMap::new(), top: Vec::new(), source_size_sequences: Vec::new(), next_link: 1 }
     }
 
     pub fn bin(&mut self, name: &str, parent: Option<BinId>) -> BinId {
@@ -512,7 +513,7 @@ impl Builder {
                 m.info.duration = end;
             }
         }
-        Imported { project: self.p, sequences: self.top }
+        Imported { project: self.p, sequences: self.top, source_size_sequences: self.source_size_sequences }
     }
 }
 
diff --git a/crates/interchange/src/lib.rs b/crates/interchange/src/lib.rs
--- a/crates/interchange/src/lib.rs
+++ b/crates/interchange/src/lib.rs
@@ -236,6 +236,8 @@ pub struct Imported {
     pub project: Project,
     /// Top-level sequences, in document order (nested sequences are items too but not listed here).
     pub sequences: Vec<ItemId>,
+    /// OTIO sequences without explicit settings, eligible for host-probed picture sizing.
+    pub source_size_sequences: Vec<ItemId>,
 }
 
 /// Import options beyond the defaults of [`import`].
@@ -424,7 +426,12 @@ pub fn export_edl_per_track(project: &Project, sequence: ItemId, opts: &ExportOp
 /// fragment's root children go into `bin` (or the root). Returns the new ids of the fragment's
 /// top-level sequences.
 pub fn merge_into(target: &mut Project, fragment: Imported, bin: Option<BinId>) -> Vec<ItemId> {
-    let Imported { project: frag, sequences } = fragment;
+    merge_with_provenance(target, fragment, bin).0
+}
+
+/// Merge a fragment, also returning remapped sequences eligible for source picture sizing.
+pub fn merge_with_provenance(target: &mut Project, fragment: Imported, bin: Option<BinId>) -> (Vec<ItemId>, Vec<ItemId>) {
+    let Imported { project: frag, sequences, source_size_sequences } = fragment;
     let mut items: HashMap<ItemId, ItemId> = HashMap::new();
     for id in frag.items.keys() {
         items.insert(*id, ItemId(target.alloc_id()));
@@ -498,7 +505,7 @@ pub fn merge_into(target: &mut Project, fragment: Imported, bin: Option<BinId>)
         None => &mut target.root,
     };
     dest.children.extend(root.children);
-    sequences.iter().filter_map(|s| items.get(s).copied()).collect()
+    (sequences.iter().filter_map(|s| items.get(s).copied()).collect(), source_size_sequences.iter().filter_map(|s| items.get(s).copied()).collect())
 }
 
 /// After probing a media file imported from an EDL (whose source times are absolute timecode),
diff --git a/crates/interchange/src/otio.rs b/crates/interchange/src/otio.rs
--- a/crates/interchange/src/otio.rs
+++ b/crates/interchange/src/otio.rs
@@ -434,9 +434,12 @@ impl Imp<'_, '_> {
     }
 
     fn sequence(&mut self, stack: &Value, name: &str, meta: Option<&Value>, rate: FrameRate) -> ItemId {
-        let settings: SequenceSettings =
-            meta.and_then(|m| m.get("settings")).and_then(|s| serde_json::from_value(s.clone()).ok()).unwrap_or_else(|| settings_for(rate, 1920, 1080, false));
+        let explicit: Option<SequenceSettings> = meta.and_then(|m| m.get("settings")).and_then(|s| serde_json::from_value(s.clone()).ok());
+        let settings = explicit.clone().unwrap_or_else(|| settings_for(rate, 1920, 1080, false));
         let id = self.b.reserve_sequence(name, settings.clone(), None);
+        if explicit.is_none() {
+            self.b.source_size_sequences.push(id);
+        }
         if let Some(l) = meta.and_then(|m| m.get("label")).and_then(|l| serde_json::from_value::<Label>(l.clone()).ok())
             && let Some(it) = self.b.p.item_mut(id)
         {
