Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3188c66
Update dependencies and API keys in worker
MathisZerbib Apr 8, 2026
9ce6646
feat: add dynamic model picker for LM Studio and cleanup wrangler.toml
MathisZerbib Apr 8, 2026
5415e71
feat: update model selection UI and add API keys to wrangler.toml
MathisZerbib Apr 8, 2026
aeb578a
fix: restore deleted code from CompanionPanelView
MathisZerbib Apr 8, 2026
9826fc9
chore: stop tracking wrangler.toml to protect secrets
MathisZerbib Apr 8, 2026
bfb5bca
chore: ignore .wrangler cache and tmp directory
MathisZerbib Apr 8, 2026
63217d0
feat: support selecting both LM Studio and Claude API models from the…
MathisZerbib Apr 8, 2026
6aaedf8
refactor: clean up spacing and organization in CompanionPanelView
MathisZerbib Apr 8, 2026
f932903
Remove middleware loader and associated files to streamline worker fu…
MathisZerbib Apr 8, 2026
13a5f12
feat: update Cloudflare Worker URLs for AssemblyAI and CompanionManag…
MathisZerbib Apr 8, 2026
9e9ae0f
chore: update .gitignore to exclude xcuserdata directory in .wrangler
MathisZerbib Apr 8, 2026
4286396
chore: remove xcuserdata from repository
MathisZerbib Apr 8, 2026
521a74c
chore: update .gitignore to include xcuserdata and its related files
MathisZerbib Apr 8, 2026
f81896e
feat: add local LLM API key support and update OpenAIAPI initialization
MathisZerbib Apr 8, 2026
fe45ffd
fix: update prompt text color for LM Studio API Key fields
MathisZerbib Apr 8, 2026
4e5db33
feat: enhance LM Studio API Key input field with placeholder and impr…
MathisZerbib Apr 8, 2026
e83c0e6
feat: update default model initialization to empty string for flexibi…
MathisZerbib Apr 8, 2026
c618820
feat: refactor API proxy URL handling and add WorkerEnvironment for d…
MathisZerbib Apr 8, 2026
344d868
feat: add language code support to transcription providers and UI for…
MathisZerbib Apr 8, 2026
7920677
feat: add Russian language option to language picker in CompanionPane…
MathisZerbib Apr 8, 2026
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ build/
releases/
.claude/
coding-plans/

/worker/wrangler.toml

/worker/.wrangler/

xcuserdata/
*.xcuserdatad/
4 changes: 2 additions & 2 deletions leanring-buddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2UDAY4J48G;
DEVELOPMENT_TEAM = ZY7Q367B6W;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
Expand Down Expand Up @@ -452,7 +452,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 2UDAY4J48G;
DEVELOPMENT_TEAM = ZY7Q367B6W;
ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
Expand Down

This file was deleted.

This file was deleted.

6 changes: 4 additions & 2 deletions leanring-buddy/AppleSpeechTranscriptionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ final class AppleSpeechTranscriptionProvider: BuddyTranscriptionProvider {
let unavailableExplanation: String? = nil

func startStreamingSession(
languageCode: String,
keyterms: [String],
onTranscriptUpdate: @escaping (String) -> Void,
onFinalTranscriptReady: @escaping (String) -> Void,
onError: @escaping (Error) -> Void
) async throws -> any BuddyStreamingTranscriptionSession {
guard let speechRecognizer = Self.makeBestAvailableSpeechRecognizer() else {
guard let speechRecognizer = Self.makeBestAvailableSpeechRecognizer(languageCode: languageCode) else {
throw AppleSpeechTranscriptionProviderError(message: "dictation is not available on this mac.")
}

Expand All @@ -41,8 +42,9 @@ final class AppleSpeechTranscriptionProvider: BuddyTranscriptionProvider {
)
}

private static func makeBestAvailableSpeechRecognizer() -> SFSpeechRecognizer? {
private static func makeBestAvailableSpeechRecognizer(languageCode: String) -> SFSpeechRecognizer? {
let preferredLocales = [
Locale(identifier: languageCode),
Locale.autoupdatingCurrent,
Locale(identifier: "en-US")
]
Expand Down
24 changes: 17 additions & 7 deletions leanring-buddy/AssemblyAIStreamingTranscriptionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ struct AssemblyAIStreamingTranscriptionProviderError: LocalizedError {
}

final class AssemblyAIStreamingTranscriptionProvider: BuddyTranscriptionProvider {
/// URL for the Cloudflare Worker endpoint that returns a short-lived
/// AssemblyAI streaming token. The real API key never leaves the server.
private static let tokenProxyURL = "https://your-worker-name.your-subdomain.workers.dev/transcribe-token"

let displayName = "AssemblyAI"
let requiresSpeechRecognitionPermission = false

Expand All @@ -34,6 +30,7 @@ final class AssemblyAIStreamingTranscriptionProvider: BuddyTranscriptionProvider
private let sharedWebSocketURLSession = URLSession(configuration: .default)

func startStreamingSession(
languageCode: String,
keyterms: [String],
onTranscriptUpdate: @escaping (String) -> Void,
onFinalTranscriptReady: @escaping (String) -> Void,
Expand All @@ -48,6 +45,7 @@ final class AssemblyAIStreamingTranscriptionProvider: BuddyTranscriptionProvider
temporaryToken: temporaryToken,
urlSession: sharedWebSocketURLSession,
keyterms: keyterms,
languageCode: languageCode,
onTranscriptUpdate: onTranscriptUpdate,
onFinalTranscriptReady: onFinalTranscriptReady,
onError: onError
Expand All @@ -59,7 +57,8 @@ final class AssemblyAIStreamingTranscriptionProvider: BuddyTranscriptionProvider

/// Calls the Cloudflare Worker to get a short-lived AssemblyAI token.
private func fetchTemporaryToken() async throws -> String {
var request = URLRequest(url: URL(string: Self.tokenProxyURL)!)
let baseURL = await WorkerEnvironment.shared.getBaseURL()
var request = URLRequest(url: URL(string: "\(baseURL)/transcribe-token")!)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.data(for: request)
Expand Down Expand Up @@ -117,6 +116,7 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre
private let apiKey: String?
private let temporaryToken: String?
private let keyterms: [String]
private let languageCode: String
private let onTranscriptUpdate: (String) -> Void
private let onFinalTranscriptReady: (String) -> Void
private let onError: (Error) -> Void
Expand All @@ -142,6 +142,7 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre
temporaryToken: String?,
urlSession: URLSession,
keyterms: [String],
languageCode: String,
onTranscriptUpdate: @escaping (String) -> Void,
onFinalTranscriptReady: @escaping (String) -> Void,
onError: @escaping (Error) -> Void
Expand All @@ -150,6 +151,7 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre
self.temporaryToken = temporaryToken
self.urlSession = urlSession
self.keyterms = keyterms
self.languageCode = languageCode
self.onTranscriptUpdate = onTranscriptUpdate
self.onFinalTranscriptReady = onFinalTranscriptReady
self.onError = onError
Expand All @@ -158,7 +160,8 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre
func open() async throws {
let websocketURL = try Self.makeWebsocketURL(
temporaryToken: temporaryToken,
keyterms: keyterms
keyterms: keyterms,
languageCode: languageCode
)

var websocketRequest = URLRequest(url: websocketURL)
Expand Down Expand Up @@ -436,7 +439,8 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre

private static func makeWebsocketURL(
temporaryToken: String?,
keyterms: [String]
keyterms: [String],
languageCode: String
) throws -> URL {
guard var websocketURLComponents = URLComponents(string: websocketBaseURLString) else {
throw AssemblyAIStreamingTranscriptionProviderError(
Expand All @@ -451,6 +455,12 @@ private final class AssemblyAIStreamingTranscriptionSession: NSObject, BuddyStre
URLQueryItem(name: "speech_model", value: "u3-rt-pro")
]

if languageCode != "en" {
// AssemblyAI supports multiple language codes.
queryItems.append(URLQueryItem(name: "language_code", value: languageCode))
// u3-rt-pro generally supports multi lingual. If any errors happen, we might need a different speech_model or drop it.
}

let normalizedKeyterms = keyterms
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
Expand Down
2 changes: 2 additions & 0 deletions leanring-buddy/BuddyDictationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,9 @@ final class BuddyDictationManager: NSObject, ObservableObject {

print("πŸŽ™οΈ BuddyDictationManager: opening transcription provider \(transcriptionProvider.displayName)")

let languageCode = UserDefaults.standard.string(forKey: "selectedLanguageCode") ?? "en"
let activeTranscriptionSession = try await transcriptionProvider.startStreamingSession(
languageCode: languageCode,
keyterms: buildTranscriptionKeyterms(),
onTranscriptUpdate: { [weak self] transcriptText in
Task { @MainActor in
Expand Down
1 change: 1 addition & 0 deletions leanring-buddy/BuddyTranscriptionProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ protocol BuddyTranscriptionProvider {
var unavailableExplanation: String? { get }

func startStreamingSession(
languageCode: String,
keyterms: [String],
onTranscriptUpdate: @escaping (String) -> Void,
onFinalTranscriptReady: @escaping (String) -> Void,
Expand Down
12 changes: 4 additions & 8 deletions leanring-buddy/ClaudeAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ class ClaudeAPI {
private static let tlsWarmupLock = NSLock()
private static var hasStartedTLSWarmup = false

private let apiURL: URL
var proxyURL: String
private var apiURL: URL { URL(string: proxyURL)! }
var model: String
private let session: URLSession

init(proxyURL: String, model: String = "claude-sonnet-4-6") {
self.apiURL = URL(string: proxyURL)!
self.proxyURL = proxyURL
self.model = model

// Use .default instead of .ephemeral so TLS session tickets are cached.
Expand All @@ -29,11 +30,6 @@ class ClaudeAPI {
config.urlCache = nil
config.httpCookieStorage = nil
self.session = URLSession(configuration: config)

// Fire a lightweight HEAD request in the background to pre-establish the TLS
// connection. This caches the TLS session ticket so the first real API call
// (which carries a large image payload) doesn't need a cold TLS handshake.
warmUpTLSConnectionIfNeeded()
}

private func makeAPIRequest() -> URLRequest {
Expand Down Expand Up @@ -63,7 +59,7 @@ class ClaudeAPI {

/// Sends a no-op HEAD request to the API host to establish and cache a TLS session.
/// Failures are silently ignored β€” this is purely an optimization.
private func warmUpTLSConnectionIfNeeded() {
func warmUpTLSConnectionIfNeeded() {
Self.tlsWarmupLock.lock()
let shouldStartTLSWarmup = !Self.hasStartedTLSWarmup
if shouldStartTLSWarmup {
Expand Down
Loading