Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,22 @@ The two tracks are versioned independently.
the tray while keeping their files on disk, then restore them individually or
all at once from Settings.

### Changed

- **Narrated-video captions are now opt-in:** Settings → Narration has a
**Burn in captions** toggle, off by default. Word-clock captions cannot stay
locked to Qwen's actual pacing.
- **Steadier narrator across a video:** each job synthesizes one short Ryan
(or chosen speaker) clip, then clones every later sentence from that same
reference so timbre does not wander between chunks.
- **Setup card after the intro:** narrated videos open the friction-log Goal
and Persona as a spoken, typeset page before the first screenshot.

### Fixed

- **Narrated video cutoff and crawl:** long step transcripts are now spoken in
sentence chunks, trailing TTS silence is trimmed before captions are timed,
and the voice is asked to talk at a conversational pace (temperature 0.5).
- **macOS dark mode:** tray, Settings, detail views, friction logs, overlays,
and review sheets now use appearance-aware backgrounds, text, borders, and
accent colors instead of mixing dark system text with fixed light cards.
Expand Down
24 changes: 16 additions & 8 deletions macos/Astroshots.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
395A03C70C98F97840F02AF8 /* MLXAudioCore in Frameworks */ = {isa = PBXBuildFile; productRef = 9AEB54D8607EFEF17F160806 /* MLXAudioCore */; };
3B7C5B7B558B59E7414950DF /* ReviewFlowUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182E7298047007414ABC9862 /* ReviewFlowUITests.swift */; };
3D70DEAA463D3459B886ED95 /* NarrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7975C24FB04A3D1A9DA4227 /* NarrationTests.swift */; };
418B4A3E6A6875B8EB6437F0 /* NarrationSpeech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E306BB2A5486653FEA77761 /* NarrationSpeech.swift */; };
4775F0ACFAFE194C51D69855 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B08FD45A80670CAD9391ADB /* AppDelegate.swift */; };
4C581895750DA89007B483FF /* NarrationPaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10EF202600AB264B34CDB2D7 /* NarrationPaths.swift */; };
51D0FFA42C1196281E94E2F5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 369606653B9FD1E6FBE4B43D /* Assets.xcassets */; };
Expand All @@ -33,9 +34,11 @@
6A8F6DA71E32DD3A01ED8141 /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2788CCB47877372A7EF96688 /* Manifest.swift */; };
76EB28B98DBFABBF892E3A7E /* FrictionLogListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33713B379610D87587D40B40 /* FrictionLogListView.swift */; };
77469FC8404628A2CE0A1894 /* astroshots-app-icon-master.png in Resources */ = {isa = PBXBuildFile; fileRef = F217601C57C50DA926C31AF2 /* astroshots-app-icon-master.png */; };
842084A0B15BCB986B44ADB1 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F5F7C7BB45AE1FD2CD699B /* ThemeTests.swift */; };
86F48D75FA114B1E154B6595 /* FrictionLogLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E835F1DA4729DEB7570B9FBC /* FrictionLogLoaderTests.swift */; };
87D9D684AF70B41C69A9A1C7 /* ReviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D528EB0C9FF62471B7103498 /* ReviewStore.swift */; };
8BB7EC1365F3F71AAD8DB5D8 /* FrictionStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A2EE3350232CD98BC7DF3D0 /* FrictionStepDetailView.swift */; };
8E263B6F9A4ED674DA7F4B2D /* NarrationBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180276769F3E77F32E31F62F /* NarrationBrief.swift */; };
8F2395DC60D12581FB418608 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9976B5034C36AABFDFD4B2C8 /* AppState.swift */; };
967A58CBD25BFB06C924D2DD /* NarrationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28BEB6B07761BB8C9BE0687 /* NarrationModels.swift */; };
96FF7A41FF2B21E652A0DB9F /* AstroshotWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976E87A9FC5F5847878A8A64 /* AstroshotWatcher.swift */; };
Expand All @@ -61,7 +64,6 @@
F4ADD26B485AABC510D0CD7E /* AstroshotWatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A1E2488E103600176B240E /* AstroshotWatcherTests.swift */; };
F57D0C06CCA71E8AED668D7D /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84934BBEECF7545EB5C02073 /* Theme.swift */; };
F8857D65FAC69AB00ED2A558 /* StreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5679FA304E48ED78A5B24170 /* StreamView.swift */; };
A11D4A2B81C643ACA8771201 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11D4A2B81C643ACA8771202 /* ThemeTests.swift */; };
FC5EB07E736FC0E1582FD805 /* ImageClipboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA37DEE55FC5E2992C0612C /* ImageClipboard.swift */; };
FF7CB55DF3385E5F06077544 /* ReviewStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9EA16401CCC820E6AD2BC9A /* ReviewStoreTests.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -90,6 +92,7 @@
0B03ABCA3ACC16E733DE7B00 /* TrayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrayView.swift; sourceTree = "<group>"; };
0B730D3DA7EA103359F22B14 /* FrictionLogDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrictionLogDetailView.swift; sourceTree = "<group>"; };
10EF202600AB264B34CDB2D7 /* NarrationPaths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NarrationPaths.swift; sourceTree = "<group>"; };
180276769F3E77F32E31F62F /* NarrationBrief.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NarrationBrief.swift; sourceTree = "<group>"; };
182E7298047007414ABC9862 /* ReviewFlowUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewFlowUITests.swift; sourceTree = "<group>"; };
2300722B17A148F2A115D3BC /* FrictionLogTrayJourneyUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrictionLogTrayJourneyUITests.swift; sourceTree = "<group>"; };
239B5944E0638F1D931913A7 /* ShotIndexCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShotIndexCache.swift; sourceTree = "<group>"; };
Expand All @@ -111,6 +114,7 @@
5239CF46065A15D17D2B56CF /* AstroshotsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstroshotsApp.swift; sourceTree = "<group>"; };
5679FA304E48ED78A5B24170 /* StreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamView.swift; sourceTree = "<group>"; };
5C52CACF01C306AEEF350902 /* NarrationVideoComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NarrationVideoComposer.swift; sourceTree = "<group>"; };
5E306BB2A5486653FEA77761 /* NarrationSpeech.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NarrationSpeech.swift; sourceTree = "<group>"; };
70B6A1CB8442F588999CF4B1 /* ShotIndexCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShotIndexCacheTests.swift; sourceTree = "<group>"; };
71AECECE88B0D8F41B06334C /* ReviewTakeoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewTakeoverView.swift; sourceTree = "<group>"; };
75955E0D26EB206BCE2025ED /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
Expand All @@ -121,6 +125,7 @@
8C127AB1558EF7894B101A53 /* StreamGroupingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamGroupingTests.swift; sourceTree = "<group>"; };
8F2AB8506FCE8107DD28D547 /* Astroshots.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Astroshots.entitlements; sourceTree = "<group>"; };
976E87A9FC5F5847878A8A64 /* AstroshotWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstroshotWatcher.swift; sourceTree = "<group>"; };
98F5F7C7BB45AE1FD2CD699B /* ThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTests.swift; sourceTree = "<group>"; };
9976B5034C36AABFDFD4B2C8 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
9E64F943CCC3A0ED542021C6 /* SoftwareUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateDelegate.swift; sourceTree = "<group>"; };
A331496EBB7CAF55CCDEF017 /* ManifestParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManifestParserTests.swift; sourceTree = "<group>"; };
Expand All @@ -142,7 +147,6 @@
E9F84D4D30E15FCC4EECC1C2 /* NarrationJobQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NarrationJobQueue.swift; sourceTree = "<group>"; };
F217601C57C50DA926C31AF2 /* astroshots-app-icon-master.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "astroshots-app-icon-master.png"; sourceTree = "<group>"; };
F9EA16401CCC820E6AD2BC9A /* ReviewStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewStoreTests.swift; sourceTree = "<group>"; };
A11D4A2B81C643ACA8771202 /* ThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -174,7 +178,7 @@
70B6A1CB8442F588999CF4B1 /* ShotIndexCacheTests.swift */,
5089282794B5A6E6B3636886 /* ShotPathTests.swift */,
8C127AB1558EF7894B101A53 /* StreamGroupingTests.swift */,
A11D4A2B81C643ACA8771202 /* ThemeTests.swift */,
98F5F7C7BB45AE1FD2CD699B /* ThemeTests.swift */,
);
path = AstroshotsTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -258,10 +262,12 @@
797776DFC44DDFA0E00939BC /* Narration */ = {
isa = PBXGroup;
children = (
180276769F3E77F32E31F62F /* NarrationBrief.swift */,
E9F84D4D30E15FCC4EECC1C2 /* NarrationJobQueue.swift */,
B879878D858221AB43F32C45 /* NarrationModelManager.swift */,
C28BEB6B07761BB8C9BE0687 /* NarrationModels.swift */,
10EF202600AB264B34CDB2D7 /* NarrationPaths.swift */,
5E306BB2A5486653FEA77761 /* NarrationSpeech.swift */,
5C52CACF01C306AEEF350902 /* NarrationVideoComposer.swift */,
);
path = Narration;
Expand Down Expand Up @@ -489,7 +495,7 @@
A1C2A497863C6149280EDF41 /* ShotIndexCacheTests.swift in Sources */,
D862C2464A53C71CC3BD45DF /* ShotPathTests.swift in Sources */,
699399301E1BE2AFEAEF81CA /* StreamGroupingTests.swift in Sources */,
A11D4A2B81C643ACA8771201 /* ThemeTests.swift in Sources */,
842084A0B15BCB986B44ADB1 /* ThemeTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -509,10 +515,12 @@
DCD8CB2F02CA756A627DDE3F /* FrictionStepTakeoverView.swift in Sources */,
FC5EB07E736FC0E1582FD805 /* ImageClipboard.swift in Sources */,
6A8F6DA71E32DD3A01ED8141 /* Manifest.swift in Sources */,
8E263B6F9A4ED674DA7F4B2D /* NarrationBrief.swift in Sources */,
1B4177ACA9522A6931715AEE /* NarrationJobQueue.swift in Sources */,
351739C9880F7D8D2B6F27EF /* NarrationModelManager.swift in Sources */,
967A58CBD25BFB06C924D2DD /* NarrationModels.swift in Sources */,
4C581895750DA89007B483FF /* NarrationPaths.swift in Sources */,
418B4A3E6A6875B8EB6437F0 /* NarrationSpeech.swift in Sources */,
AC966A901816D42D9B701C48 /* NarrationVideoComposer.swift in Sources */,
BB6D913C2F3291617685815E /* OverlayController.swift in Sources */,
17BFA74EE76E1E71A5627F33 /* OverlayView.swift in Sources */,
Expand Down Expand Up @@ -648,7 +656,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 23;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO;
Expand All @@ -662,7 +670,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.2.5;
MARKETING_VERSION = 0.2.6;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -762,7 +770,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 23;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
Expand All @@ -782,7 +790,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.2.5;
MARKETING_VERSION = 0.2.6;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down
14 changes: 13 additions & 1 deletion macos/Astroshots/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ final class AppState {
/// Opt-in narrated friction-log videos (MLX Audio + Qwen3-TTS).
var narrationEnabled: Bool
var narrationVoice: String
var narrationCaptionsEnabled: Bool
let narration: NarrationModelManager
let narrationQueue: NarrationJobQueue
var watchRootPaths: [String]
Expand Down Expand Up @@ -160,6 +161,7 @@ final class AppState {
autoDismiss = preferences.autoDismiss
narrationEnabled = preferences.narrationEnabled
narrationVoice = preferences.narrationVoice
narrationCaptionsEnabled = preferences.narrationCaptionsEnabled
hiddenFrictionLogIDs = preferences.hiddenFrictionLogIDs
watchRootPaths = rootPaths
needsWatchRootSetup = needsSetup
Expand Down Expand Up @@ -601,10 +603,20 @@ final class AppState {
preferences.narrationVoice = normalized
}

func setNarrationCaptionsEnabled(_ enabled: Bool) {
narrationCaptionsEnabled = enabled
preferences.narrationCaptionsEnabled = enabled
}

@discardableResult
func enqueueNarration(for log: FrictionLog, run: FrictionLogRun) -> Bool {
do {
_ = try narrationQueue.enqueue(log: log, run: run, voice: narrationVoice)
_ = try narrationQueue.enqueue(
log: log,
run: run,
voice: narrationVoice,
showCaptions: narrationCaptionsEnabled
)
showToast("Narration queued")
return true
} catch {
Expand Down
9 changes: 9 additions & 0 deletions macos/Astroshots/App/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ final class Preferences {
static let narrationModelReady = "narrationModelReady"
/// Named Qwen3 CustomVoice speaker used for previews and every render.
static let narrationVoice = "narrationVoice"
/// Burned-in captions on narrated MP4s. Default off: the word clock
/// cannot stay locked to Qwen's actual pacing.
static let narrationCaptionsEnabled = "narrationCaptionsEnabled"
}

/// Migration schema for first-run detection. Increment when adoption rules
Expand Down Expand Up @@ -148,6 +151,12 @@ final class Preferences {
set { defaults.set(NarrationVoice.normalized(newValue), forKey: Key.narrationVoice) }
}

/// Missing key is off (`UserDefaults.bool` default).
var narrationCaptionsEnabled: Bool {
get { defaults.bool(forKey: Key.narrationCaptionsEnabled) }
set { defaults.set(newValue, forKey: Key.narrationCaptionsEnabled) }
}

/// Call when the user successfully chooses initial watch folders.
func markFirstRunSetupComplete() {
hasCompletedFirstRunSetup = true
Expand Down
7 changes: 7 additions & 0 deletions macos/Astroshots/Features/Tray/SettingsPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ struct SettingsPane: View {
.overlay(alignment: .top) {
Rectangle().fill(Theme.line).frame(height: 1)
}

toggleRow(
"Burn in captions",
isOn: appState.narrationCaptionsEnabled
) { enabled in
appState.setNarrationCaptionsEnabled(enabled)
}
}

VStack(alignment: .leading, spacing: 6) {
Expand Down
18 changes: 18 additions & 0 deletions macos/Astroshots/Models/FrictionLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,24 @@ enum FrictionLogLoader {
var captured_at: String?
}

/// Load the log + run that own a `runs/<id>/` directory.
static func loadRun(directory: URL) -> (FrictionLog, FrictionLogRun)? {
let runDir = directory.standardizedFileURL
let slugDir = runDir.deletingLastPathComponent().deletingLastPathComponent()
let frictionRoot = slugDir.deletingLastPathComponent()
let astroshot = frictionRoot.deletingLastPathComponent()
guard frictionRoot.lastPathComponent == FrictionLogPath.directoryName,
astroshot.lastPathComponent == ShotPath.astroshotDirName
else { return nil }
let logs = loadLogs(inAstroshot: astroshot)
let runID = runDir.lastPathComponent
let slug = slugDir.lastPathComponent
guard let log = logs.first(where: { $0.slug == slug }),
let run = log.runs.first(where: { $0.runID == runID })
else { return nil }
return (log, run)
}

/// Scan `.astroshot/friction-logs/*` under a single `.astroshot` directory.
static func loadLogs(inAstroshot astroshot: URL) -> [FrictionLog] {
let root = FrictionLogPath.root(inAstroshot: astroshot)
Expand Down
Loading
Loading