diff --git a/ModelCraft.xcodeproj/project.pbxproj b/ModelCraft.xcodeproj/project.pbxproj index 8b5c3f4..100aac6 100644 --- a/ModelCraft.xcodeproj/project.pbxproj +++ b/ModelCraft.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 70271B3A2BC0455000E21A6E /* ollama in CopyFiles */ = {isa = PBXBuildFile; fileRef = 70271B382BC0452C00E21A6E /* ollama */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 70271B3C2BC046F300E21A6E /* KnowledgaBaseModelActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70271B3B2BC046F300E21A6E /* KnowledgaBaseModelActor.swift */; }; 70271B5E2BC1A97900E21A6E /* ModelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70271B5D2BC1A97900E21A6E /* ModelStore.swift */; }; + 70271B782BC4412E00E21A6E /* SpeechRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70271B772BC4412E00E21A6E /* SpeechRecognizer.swift */; }; 703422112BB6A38C00332E04 /* OllamaKit in Frameworks */ = {isa = PBXBuildFile; productRef = 703422102BB6A38C00332E04 /* OllamaKit */; }; 703845832BB6674600E3128B /* Locale+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 703845822BB6674600E3128B /* Locale+.swift */; }; 7041EA702BB2AACB00286AE3 /* EnvironmentValues+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7041EA6F2BB2AACB00286AE3 /* EnvironmentValues+.swift */; }; @@ -110,6 +111,7 @@ 70271B382BC0452C00E21A6E /* ollama */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = ollama; sourceTree = ""; }; 70271B3B2BC046F300E21A6E /* KnowledgaBaseModelActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnowledgaBaseModelActor.swift; sourceTree = ""; }; 70271B5D2BC1A97900E21A6E /* ModelStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelStore.swift; sourceTree = ""; }; + 70271B772BC4412E00E21A6E /* SpeechRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechRecognizer.swift; sourceTree = ""; }; 703845822BB6674600E3128B /* Locale+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+.swift"; sourceTree = ""; }; 7041EA6F2BB2AACB00286AE3 /* EnvironmentValues+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+.swift"; sourceTree = ""; }; 7041EA712BB2AC8D00286AE3 /* ServerStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStatusView.swift; sourceTree = ""; }; @@ -205,6 +207,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 70271B792BC4E2CC00E21A6E /* Models */ = { + isa = PBXGroup; + children = ( + 70C22FC72BAECD55002754C7 /* Default.swift */, + 70E201842BAEC08900CFE1D7 /* SplashCodeSyntaxHighlighter.swift */, + 70E201862BAEC0C700CFE1D7 /* TextOutputFormat.swift */, + 70B13D032BBB1F3300A42F3C /* XMLFile.swift */, + 70CE4D282BB83BF500E1DEE2 /* Pasteboard.swift */, + 70271B3B2BC046F300E21A6E /* KnowledgaBaseModelActor.swift */, + 70271B772BC4412E00E21A6E /* SpeechRecognizer.swift */, + ); + path = Models; + sourceTree = ""; + }; 706D729D2BB6B076004ACF35 /* Services */ = { isa = PBXGroup; children = ( @@ -231,13 +247,6 @@ path = SwiftData; sourceTree = ""; }; - 7097F9672BB0234E001A91E3 /* Utils */ = { - isa = PBXGroup; - children = ( - ); - path = Utils; - sourceTree = ""; - }; 70A574AE2BB0997A008E3750 /* Views */ = { isa = PBXGroup; children = ( @@ -371,21 +380,15 @@ 70CE606F2BAD2D4600A2D0B0 /* ModelCraft.entitlements */, 7045BF462BB3FE0A00CE61D1 /* Info.plist */, 70CE60692BAD2D4500A2D0B0 /* ContentView.swift */, - 70C22FC72BAECD55002754C7 /* Default.swift */, 70CE60672BAD2D4500A2D0B0 /* ModelCraftApp.swift */, - 70CE4D282BB83BF500E1DEE2 /* Pasteboard.swift */, - 70E201842BAEC08900CFE1D7 /* SplashCodeSyntaxHighlighter.swift */, - 70E201862BAEC0C700CFE1D7 /* TextOutputFormat.swift */, - 70B13D032BBB1F3300A42F3C /* XMLFile.swift */, 70CE606D2BAD2D4600A2D0B0 /* Assets.xcassets */, 70A574A92BB07264008E3750 /* Localizable.xcstrings */, 70C22FD52BAFD1AA002754C7 /* Components */, 70CE609A2BAD302000A2D0B0 /* Extension */, 70CE60C92BAD7A4700A2D0B0 /* Features */, + 70271B792BC4E2CC00E21A6E /* Models */, 70CE60702BAD2D4600A2D0B0 /* Preview Content */, 706D729D2BB6B076004ACF35 /* Services */, - 7097F9672BB0234E001A91E3 /* Utils */, - 70271B3B2BC046F300E21A6E /* KnowledgaBaseModelActor.swift */, ); path = ModelCraft; sourceTree = ""; @@ -660,6 +663,7 @@ 7045BF452BB314E700CE61D1 /* Bundle+.swift in Sources */, 70B13D152BBC678400A42F3C /* KnowledgeBaseDetailGridView.swift in Sources */, 70C22FE92BAFD5CA002754C7 /* ImageButton.swift in Sources */, + 70271B782BC4412E00E21A6E /* SpeechRecognizer.swift in Sources */, 70CE4D312BB9500000E1DEE2 /* KnowledgeBaseDetailView.swift in Sources */, 70CE60682BAD2D4500A2D0B0 /* ModelCraftApp.swift in Sources */, ); @@ -833,6 +837,8 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ModelCraft/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "The app wants to access your microphone."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "You can chat with models in the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -876,6 +882,8 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ModelCraft/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "The app wants to access your microphone."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "You can chat with models in the app."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; diff --git a/ModelCraft/Features/KnowledgeBase/Views/KnowledgeBaseEdition.swift b/ModelCraft/Features/KnowledgeBase/Views/KnowledgeBaseEdition.swift index d3903e1..048800c 100644 --- a/ModelCraft/Features/KnowledgeBase/Views/KnowledgeBaseEdition.swift +++ b/ModelCraft/Features/KnowledgeBase/Views/KnowledgeBaseEdition.swift @@ -110,7 +110,7 @@ extension KnowledgeBaseEdition { extension KnowledgeBaseEdition { func save() { - dismiss.callAsFunction() + dismiss() Task { await KnowledgaBaseModelActor(modelContainer: modelContext.container).insert(konwledgeBase) } diff --git a/ModelCraft/Features/Model/LocalModelsView.swift b/ModelCraft/Features/Model/LocalModelsView.swift index dfb352d..e65d603 100644 --- a/ModelCraft/Features/Model/LocalModelsView.swift +++ b/ModelCraft/Features/Model/LocalModelsView.swift @@ -24,12 +24,23 @@ struct LocalModelsView: View { sort: \ModelTask.createdAt, order: .reverse) private var deleteTasks: [ModelTask] = [] + @Query(filter: ModelTask.predicateByType(.download), + sort: \ModelTask.createdAt, + order: .reverse) + private var downloadTasks: [ModelTask] = [] var body: some View { List(selection: $selectedModelNames) { ForEach(models, id: \.name) { model in ListCell(model).tag(model.name) } + ForEach(downloadTasks) { task in + HStack{ + Label(task.modelName, systemImage: "shippingbox") + Spacer() + ModelDownloadProgress(task: task) + }.tag(task) + }.foregroundStyle(.secondary) } .contextMenu { DeleteButton(action: { confirmationDialogPresented = true }) diff --git a/ModelCraft/Features/Model/ModelDetailView.swift b/ModelCraft/Features/Model/ModelDetailView.swift index f0cd7d8..555794c 100644 --- a/ModelCraft/Features/Model/ModelDetailView.swift +++ b/ModelCraft/Features/Model/ModelDetailView.swift @@ -68,6 +68,7 @@ extension ModelDetailView { Button("Refresh", systemImage: "arrow.triangle.2.circlepath") { fetchModels() } + } } } diff --git a/ModelCraft/Localizable.xcstrings b/ModelCraft/Localizable.xcstrings index c18bd05..d881eab 100644 --- a/ModelCraft/Localizable.xcstrings +++ b/ModelCraft/Localizable.xcstrings @@ -1,13 +1,21 @@ { - "version" : "1.0", "sourceLanguage" : "en", "strings" : { - "Download" : { + "" : { + + }, + "%@ / %@" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ / %2$@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "下载" + "value" : "%1$@ / %2$@" } } } @@ -22,442 +30,431 @@ } } }, - "Check" : { + "Add Files" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "检查" + "value" : "添加文件" } } } }, - "No Available Model" : { + "Appearance" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "没有可用的模型", - "state" : "translated" + "state" : "translated", + "value" : "外观" } } } }, - "Status" : { + "Are you sure to delete these models" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "状态", - "state" : "translated" + "state" : "translated", + "value" : "你确定要删除这些模型吗?" } } } }, - "%@ \/ %@" : { + "Assistant" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ \/ %2$@" - } - }, - "en" : { - "stringUnit" : { - "value" : "%1$@ \/ %2$@", - "state" : "new" + "value" : "助手" } } } }, - "Are you sure to delete these models" : { + "Cancel" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "你确定要删除这些模型吗?" + "value" : "取消" } } } }, - "Failed" : { + "Chat" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "失败", - "state" : "translated" + "state" : "translated", + "value" : "对话" } } } }, - "Stopped" : { - - }, - "Choose a model to chat" : { - "extractionState" : "stale", + "Check" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "选择一个模型进行对话", - "state" : "translated" + "state" : "translated", + "value" : "检查" } } } }, - "Waiting to delete" : { + "Choose a model to chat" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "等待删除" + "value" : "选择一个模型进行对话" } } } }, - "Downloading..." : { + "Completed" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "下载中", - "state" : "translated" + "state" : "translated", + "value" : "已完成" } } } }, - "Personalization" : { + "Connected" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "定制化" + "value" : "已连接" } } } }, - "Knowledge Base" : { + "Copy" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "知识库" + "value" : "复制" } } } }, - "Appearance" : { + "Dark" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "外观" + "value" : "深色" } } } }, - "Speaking Rate" : { + "Delete" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "说话速度" + "value" : "删除" } } } }, - "Thread" : { - "extractionState" : "stale", + "Deleting..." : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "线程", - "state" : "translated" + "state" : "translated", + "value" : "删除中" } } } }, - "Refresh" : { + "Disconnected" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "刷新" + "value" : "断开连接" } } } }, - "What would you like model to know about you to provide better responses ?" : { + "Download" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "为了提供更好的回答,您希望模型了解您的哪些信息?" + "value" : "下载" } } } }, - "Grid" : { + "Downloaded" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "网格" + "value" : "已下载" } } } }, - "Open %@" : { - "extractionState" : "stale", + "Downloading..." : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "打开 %@", - "state" : "translated" + "state" : "translated", + "value" : "下载中" } } } }, - "How can I help you today ?" : { + "Edit" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "我能帮您什么吗?" + "value" : "编辑" } } } }, - "Indexing..." : { - - }, - "Open ModelStore" : { + "Explore and download models to chat." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "打开", - "state" : "translated" + "state" : "translated", + "value" : "浏览并下载模型进行对话" } } - }, - "extractionState" : "stale" + } }, - "You" : { + "Failed" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "你" + "value" : "失败" } } } }, - "Explore and download models to chat." : { - "extractionState" : "stale", + "General" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "浏览并下载模型进行对话", - "state" : "translated" + "state" : "translated", + "value" : "通用" } } } }, - "Today" : { + "Grid" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "今天" + "value" : "网格" } } } }, - "Model Store" : { + "How can I help you today ?" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "模型商店" + "value" : "我能帮您什么吗?" } } } }, - "General" : { + "How would you like model to respond ?" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "通用", - "state" : "translated" + "state" : "translated", + "value" : "你想要模型如何回答?" } } } }, - "Speaking Volume" : { + "Indexing..." : { + + }, + "Knowledge Base" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "说话音量" + "value" : "知识库" } } } }, - "Completed" : { + "Language" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "已完成" + "value" : "语言" } } } }, - "Assistant" : { + "Launching" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "助手" + "value" : "启动中" } } } }, - "Select or" : { + "Light" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "选择或者", - "state" : "translated" + "state" : "translated", + "value" : "浅色" } } - }, - "extractionState" : "stale" + } }, - "Copy" : { + "List" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "复制", - "state" : "translated" + "state" : "translated", + "value" : "列表" } } } }, - "Save" : { + "Local Models" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "保存", - "state" : "translated" + "state" : "translated", + "value" : "本地模型" } } } }, - "Add Files" : { + "Message" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "添加文件", - "state" : "translated" + "state" : "translated", + "value" : "信息" } } } }, - "Show menu bar icon" : { - "extractionState" : "stale", + "Model" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "显示菜单栏图标" + "value" : "模型" } } } }, - "Language" : { + "Model Store" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "语言" + "value" : "模型商店" } } } }, - "Downloaded" : { + "Models" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "已下载", - "state" : "translated" + "state" : "translated", + "value" : "模型" } } } }, - "None" : { + "New Chat" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "无", - "state" : "translated" + "state" : "translated", + "value" : "新的对话" } } } }, - "Quit" : { + "No Available Model" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "退出" + "value" : "没有可用的模型" } } - }, - "extractionState" : "stale" + } }, - "Yesterday" : { + "None" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "昨天", - "state" : "translated" + "state" : "translated", + "value" : "无" } } } }, - "Model" : { + "Open %@" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "模型" + "value" : "打开 %@" } } } }, - "Models" : { + "Open ModelStore" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "模型", - "state" : "translated" + "state" : "translated", + "value" : "打开" } } } }, - "Connected" : { + "Personalization" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "已连接", - "state" : "translated" + "state" : "translated", + "value" : "定制化" } } } }, - "Light" : { + "Quit" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "浅色" + "value" : "退出" } } } @@ -472,203 +469,206 @@ } } }, - "List" : { + "Refresh" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "列表" + "value" : "刷新" } } } }, - "Waiting to download" : { + "Retry" : { + + }, + "Save" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "等待下载", - "state" : "translated" + "state" : "translated", + "value" : "保存" } } } }, - "Chat" : { + "Scroll to bottom automatically when chatting" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "对话", - "state" : "translated" + "state" : "translated", + "value" : "对话时自动滚动到底部" } } } }, - "Deleting..." : { + "Select models to download" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "删除中", - "state" : "translated" + "state" : "translated", + "value" : "选择模型下载" } } } }, - "Dark" : { + "Select or" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "深色" + "value" : "选择或者" } } } }, - "Message" : { + "Show menu bar icon" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "信息", - "state" : "translated" + "state" : "translated", + "value" : "显示菜单栏图标" } } } }, - "Disconnected" : { + "Speaking Rate" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "断开连接" + "value" : "说话速度" } } } }, - "Title" : { + "Speaking Volume" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "标题", - "state" : "translated" + "state" : "translated", + "value" : "说话音量" } } } }, - "Scroll to bottom automatically when chatting" : { + "Status" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "对话时自动滚动到底部" + "value" : "状态" } } } }, - "Select models to download" : { - "extractionState" : "stale", + "Stopped" : { + + }, + "System" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "选择模型下载", - "state" : "translated" + "state" : "translated", + "value" : "系统" } } } }, - "Retry" : { - - }, - "System" : { + "Thread" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "系统", - "state" : "translated" + "state" : "translated", + "value" : "线程" } } } }, - "View" : { + "Title" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "视图", - "state" : "translated" + "state" : "translated", + "value" : "标题" } } } }, - "" : { - - }, - "Cancel" : { + "Today" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "取消", - "state" : "translated" + "state" : "translated", + "value" : "今天" } } } }, - "Launching" : { + "View" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "启动中" + "value" : "视图" } } } }, - "How would you like model to respond ?" : { + "Waiting to delete" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "你想要模型如何回答?" + "value" : "等待删除" } } } }, - "Local Models" : { + "Waiting to download" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "本地模型" + "value" : "等待下载" } } } }, - "Delete" : { + "What would you like model to know about you to provide better responses ?" : { "localizations" : { "zh-Hans" : { "stringUnit" : { - "value" : "删除", - "state" : "translated" + "state" : "translated", + "value" : "为了提供更好的回答,您希望模型了解您的哪些信息?" } } } }, - "New Chat" : { + "Yesterday" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新的对话" + "value" : "昨天" } } - }, - "extractionState" : "stale" + } }, - "Edit" : { + "You" : { "localizations" : { "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "编辑" + "value" : "你" } } } } - } + }, + "version" : "1.0" } \ No newline at end of file diff --git a/ModelCraft/Default.swift b/ModelCraft/Models/Default.swift similarity index 100% rename from ModelCraft/Default.swift rename to ModelCraft/Models/Default.swift diff --git a/ModelCraft/KnowledgaBaseModelActor.swift b/ModelCraft/Models/KnowledgaBaseModelActor.swift similarity index 100% rename from ModelCraft/KnowledgaBaseModelActor.swift rename to ModelCraft/Models/KnowledgaBaseModelActor.swift diff --git a/ModelCraft/Pasteboard.swift b/ModelCraft/Models/Pasteboard.swift similarity index 100% rename from ModelCraft/Pasteboard.swift rename to ModelCraft/Models/Pasteboard.swift diff --git a/ModelCraft/Models/SpeechRecognizer.swift b/ModelCraft/Models/SpeechRecognizer.swift new file mode 100644 index 0000000..10a4a13 --- /dev/null +++ b/ModelCraft/Models/SpeechRecognizer.swift @@ -0,0 +1,205 @@ +// +// SpeechRecognizer.swift +// ModelCraft +// +// Created by 张鸿燊 on 8/4/2024. +// + +import Foundation +import AVFoundation +import Speech +import SwiftUI + + +/// A helper for transcribing speech to text using SFSpeechRecognizer and AVAudioEngine. +actor SpeechRecognizer: ObservableObject { + enum RecognizerError: Error { + case nilRecognizer + case notAuthorizedToRecognize + case notPermittedToRecord + case recognizerIsUnavailable + + var message: String { + switch self { + case .nilRecognizer: return "Can't initialize speech recognizer" + case .notAuthorizedToRecognize: return "Not authorized to recognize speech" + case .notPermittedToRecord: return "Not permitted to record audio" + case .recognizerIsUnavailable: return "Recognizer is unavailable" + } + } + } + + @MainActor var transcript: String = "" + + private var audioEngine: AVAudioEngine? + private var request: SFSpeechAudioBufferRecognitionRequest? + private var task: SFSpeechRecognitionTask? + private let recognizer: SFSpeechRecognizer? + + /** + Initializes a new speech recognizer. If this is the first time you've used the class, it + requests access to the speech recognizer and the microphone. + */ + init() { + recognizer = SFSpeechRecognizer() + guard recognizer != nil else { + transcribe(RecognizerError.nilRecognizer) + return + } + + Task { + do { + guard await SFSpeechRecognizer.hasAuthorizationToRecognize() else { + throw RecognizerError.notAuthorizedToRecognize + } +#if os(macOS) + guard AVCaptureDevice.hasPermissionToRecord() else { + throw RecognizerError.notPermittedToRecord + } +#else + guard await AVAudioSession.sharedInstance().hasPermissionToRecord() else { + throw RecognizerError.notPermittedToRecord + } +#endif + } catch { + transcribe(error) + } + } + } + + @MainActor func startTranscribing() { + Task { + await transcribe() + } + } + + @MainActor func resetTranscript() { + Task { + await reset() + } + } + + @MainActor func stopTranscribing() { + Task { + await reset() + } + } + + /** + Begin transcribing audio. + + Creates a `SFSpeechRecognitionTask` that transcribes speech to text until you call `stopTranscribing()`. + The resulting transcription is continuously written to the published `transcript` property. + */ + private func transcribe() { + guard let recognizer, recognizer.isAvailable else { + self.transcribe(RecognizerError.recognizerIsUnavailable) + return + } + + do { + let (audioEngine, request) = try Self.prepareEngine() + self.audioEngine = audioEngine + self.request = request + self.task = recognizer.recognitionTask(with: request, resultHandler: { [weak self] result, error in + self?.recognitionHandler(audioEngine: audioEngine, result: result, error: error) + }) + } catch { + self.reset() + self.transcribe(error) + } + } + + /// Reset the speech recognizer. + private func reset() { + task?.cancel() + audioEngine?.stop() + audioEngine = nil + request = nil + task = nil + } + + private static func prepareEngine() throws -> (AVAudioEngine, SFSpeechAudioBufferRecognitionRequest) { + let audioEngine = AVAudioEngine() + + let request = SFSpeechAudioBufferRecognitionRequest() + request.shouldReportPartialResults = true + + let inputNode = audioEngine.inputNode + + let recordingFormat = inputNode.outputFormat(forBus: 0) + inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in + request.append(buffer) + } + audioEngine.prepare() + try audioEngine.start() + + return (audioEngine, request) + } + + nonisolated private func recognitionHandler(audioEngine: AVAudioEngine, result: SFSpeechRecognitionResult?, error: Error?) { + let receivedFinalResult = result?.isFinal ?? false + let receivedError = error != nil + + if receivedFinalResult || receivedError { + audioEngine.stop() + audioEngine.inputNode.removeTap(onBus: 0) + } + + if let result { + transcribe(result.bestTranscription.formattedString) + } + } + + + nonisolated private func transcribe(_ message: String) { + Task { @MainActor in + transcript = message + } + } + nonisolated private func transcribe(_ error: Error) { + var errorMessage = "" + if let error = error as? RecognizerError { + errorMessage += error.message + } else { + errorMessage += error.localizedDescription + } + Task { @MainActor [errorMessage] in + transcript = "<< \(errorMessage) >>" + } + } +} + + +extension SFSpeechRecognizer { + static func hasAuthorizationToRecognize() async -> Bool { + await withCheckedContinuation { continuation in + requestAuthorization { status in + continuation.resume(returning: status == .authorized) + } + } + } +} + +#if os(macOS) +extension AVCaptureDevice { + static func hasPermissionToRecord() -> Bool { + switch authorizationStatus(for: .audio) { + case .authorized: true + case .notDetermined: false + default: false + } + } +} +#else +extension AVAudioSession { + func hasPermissionToRecord() async -> Bool { + await withCheckedContinuation { continuation in + requestRecordPermission { authorized in + continuation.resume(returning: authorized) + } + } + } +} +#endif + diff --git a/ModelCraft/SplashCodeSyntaxHighlighter.swift b/ModelCraft/Models/SplashCodeSyntaxHighlighter.swift similarity index 100% rename from ModelCraft/SplashCodeSyntaxHighlighter.swift rename to ModelCraft/Models/SplashCodeSyntaxHighlighter.swift diff --git a/ModelCraft/TextOutputFormat.swift b/ModelCraft/Models/TextOutputFormat.swift similarity index 100% rename from ModelCraft/TextOutputFormat.swift rename to ModelCraft/Models/TextOutputFormat.swift diff --git a/ModelCraft/XMLFile.swift b/ModelCraft/Models/XMLFile.swift similarity index 100% rename from ModelCraft/XMLFile.swift rename to ModelCraft/Models/XMLFile.swift diff --git a/README.md b/README.md index 434f3a5..d69317b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,12 @@
ModelCraft
-# ModelCraft +#ModelCraft ModelCraft is a macOS RAG app. It allows you to run models locally and build your own local knowledge base. ## 🚀 Get Started -1. make sure that [Ollama](https://github.com/ollama/ollama) is running. - -```shell -brew install ollama -brew services start ollama -``` - -2. open app +No pre-operations. Just download the app to use it ! ### Customize chat by selecting model and knowledge base.