From 84c28ad1c252553bd2131295d6e2b504fa561689 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 16 Mar 2026 18:02:20 +0800 Subject: [PATCH] fix: bug --- Aural.xcodeproj/project.pbxproj | 596 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 20633 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 85 +++ Aural/Assets.xcassets/Contents.json | 6 + Aural/AudioRouteManager.swift | 129 ++++ Aural/Aural.entitlements | 10 + Aural/AuralApp.swift | 29 + Aural/ContentView.swift | 179 ++++++ .../Preview Assets.xcassets/Contents.json | 6 + Aural/SpeechRecognitionManager.swift | 252 ++++++++ Aural/TranscriptModels.swift | 40 ++ AuralTests/AuralTests.swift | 16 + AuralUITests/AuralUITests.swift | 43 ++ AuralUITests/AuralUITestsLaunchTests.swift | 33 + README.md | 68 ++ 18 files changed, 1524 insertions(+) create mode 100644 Aural.xcodeproj/project.pbxproj create mode 100644 Aural.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Aural.xcodeproj/project.xcworkspace/xcuserdata/dannier.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Aural.xcodeproj/xcuserdata/dannier.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Aural/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Aural/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Aural/Assets.xcassets/Contents.json create mode 100644 Aural/AudioRouteManager.swift create mode 100644 Aural/Aural.entitlements create mode 100644 Aural/AuralApp.swift create mode 100644 Aural/ContentView.swift create mode 100644 Aural/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Aural/SpeechRecognitionManager.swift create mode 100644 Aural/TranscriptModels.swift create mode 100644 AuralTests/AuralTests.swift create mode 100644 AuralUITests/AuralUITests.swift create mode 100644 AuralUITests/AuralUITestsLaunchTests.swift diff --git a/Aural.xcodeproj/project.pbxproj b/Aural.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a514b95 --- /dev/null +++ b/Aural.xcodeproj/project.pbxproj @@ -0,0 +1,596 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + 7D8922492F6800BE001184E1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7D89222F2F6800BB001184E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7D8922362F6800BB001184E1; + remoteInfo = Aural; + }; + 7D8922532F6800BE001184E1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7D89222F2F6800BB001184E1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7D8922362F6800BB001184E1; + remoteInfo = Aural; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7D8922372F6800BB001184E1 /* Aural.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Aural.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D8922482F6800BE001184E1 /* AuralTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuralTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D8922522F6800BE001184E1 /* AuralUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuralUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7D8922392F6800BB001184E1 /* Aural */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Aural; + sourceTree = ""; + }; + 7D89224B2F6800BE001184E1 /* AuralTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AuralTests; + sourceTree = ""; + }; + 7D8922552F6800BF001184E1 /* AuralUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = AuralUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7D8922342F6800BB001184E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8922452F6800BE001184E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D89224F2F6800BE001184E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7D89222E2F6800BB001184E1 = { + isa = PBXGroup; + children = ( + 7D8922392F6800BB001184E1 /* Aural */, + 7D89224B2F6800BE001184E1 /* AuralTests */, + 7D8922552F6800BF001184E1 /* AuralUITests */, + 7D8922382F6800BB001184E1 /* Products */, + ); + sourceTree = ""; + }; + 7D8922382F6800BB001184E1 /* Products */ = { + isa = PBXGroup; + children = ( + 7D8922372F6800BB001184E1 /* Aural.app */, + 7D8922482F6800BE001184E1 /* AuralTests.xctest */, + 7D8922522F6800BE001184E1 /* AuralUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7D8922362F6800BB001184E1 /* Aural */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D89225C2F6800BF001184E1 /* Build configuration list for PBXNativeTarget "Aural" */; + buildPhases = ( + 7D8922332F6800BB001184E1 /* Sources */, + 7D8922342F6800BB001184E1 /* Frameworks */, + 7D8922352F6800BB001184E1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 7D8922392F6800BB001184E1 /* Aural */, + ); + name = Aural; + packageProductDependencies = ( + ); + productName = Aural; + productReference = 7D8922372F6800BB001184E1 /* Aural.app */; + productType = "com.apple.product-type.application"; + }; + 7D8922472F6800BE001184E1 /* AuralTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D89225F2F6800BF001184E1 /* Build configuration list for PBXNativeTarget "AuralTests" */; + buildPhases = ( + 7D8922442F6800BE001184E1 /* Sources */, + 7D8922452F6800BE001184E1 /* Frameworks */, + 7D8922462F6800BE001184E1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7D89224A2F6800BE001184E1 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7D89224B2F6800BE001184E1 /* AuralTests */, + ); + name = AuralTests; + packageProductDependencies = ( + ); + productName = AuralTests; + productReference = 7D8922482F6800BE001184E1 /* AuralTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7D8922512F6800BE001184E1 /* AuralUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D8922622F6800BF001184E1 /* Build configuration list for PBXNativeTarget "AuralUITests" */; + buildPhases = ( + 7D89224E2F6800BE001184E1 /* Sources */, + 7D89224F2F6800BE001184E1 /* Frameworks */, + 7D8922502F6800BE001184E1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7D8922542F6800BE001184E1 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7D8922552F6800BF001184E1 /* AuralUITests */, + ); + name = AuralUITests; + packageProductDependencies = ( + ); + productName = AuralUITests; + productReference = 7D8922522F6800BE001184E1 /* AuralUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7D89222F2F6800BB001184E1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 7D8922362F6800BB001184E1 = { + CreatedOnToolsVersion = 16.2; + }; + 7D8922472F6800BE001184E1 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 7D8922362F6800BB001184E1; + }; + 7D8922512F6800BE001184E1 = { + CreatedOnToolsVersion = 16.2; + TestTargetID = 7D8922362F6800BB001184E1; + }; + }; + }; + buildConfigurationList = 7D8922322F6800BB001184E1 /* Build configuration list for PBXProject "Aural" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7D89222E2F6800BB001184E1; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 7D8922382F6800BB001184E1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7D8922362F6800BB001184E1 /* Aural */, + 7D8922472F6800BE001184E1 /* AuralTests */, + 7D8922512F6800BE001184E1 /* AuralUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7D8922352F6800BB001184E1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8922462F6800BE001184E1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8922502F6800BE001184E1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7D8922332F6800BB001184E1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D8922442F6800BE001184E1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D89224E2F6800BE001184E1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7D89224A2F6800BE001184E1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7D8922362F6800BB001184E1 /* Aural */; + targetProxy = 7D8922492F6800BE001184E1 /* PBXContainerItemProxy */; + }; + 7D8922542F6800BE001184E1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7D8922362F6800BB001184E1 /* Aural */; + targetProxy = 7D8922532F6800BE001184E1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7D89225A2F6800BF001184E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7D89225B2F6800BF001184E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 7D89225D2F6800BF001184E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Aural/Aural.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Aural/Preview Content\""; + DEVELOPMENT_TEAM = 3882NS6655; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.Aural; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 7D89225E2F6800BF001184E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Aural/Aural.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Aural/Preview Content\""; + DEVELOPMENT_TEAM = 3882NS6655; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.Aural; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + 7D8922602F6800BF001184E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3882NS6655; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.AuralTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aural.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aural"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 7D8922612F6800BF001184E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3882NS6655; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.AuralTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Aural.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Aural"; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; + 7D8922632F6800BF001184E1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3882NS6655; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.AuralUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = Aural; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Debug; + }; + 7D8922642F6800BF001184E1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 3882NS6655; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + MACOSX_DEPLOYMENT_TARGET = 14.7; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = Bimwe.AuralUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = Aural; + XROS_DEPLOYMENT_TARGET = 2.2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7D8922322F6800BB001184E1 /* Build configuration list for PBXProject "Aural" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D89225A2F6800BF001184E1 /* Debug */, + 7D89225B2F6800BF001184E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D89225C2F6800BF001184E1 /* Build configuration list for PBXNativeTarget "Aural" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D89225D2F6800BF001184E1 /* Debug */, + 7D89225E2F6800BF001184E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D89225F2F6800BF001184E1 /* Build configuration list for PBXNativeTarget "AuralTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D8922602F6800BF001184E1 /* Debug */, + 7D8922612F6800BF001184E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D8922622F6800BF001184E1 /* Build configuration list for PBXNativeTarget "AuralUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D8922632F6800BF001184E1 /* Debug */, + 7D8922642F6800BF001184E1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7D89222F2F6800BB001184E1 /* Project object */; +} diff --git a/Aural.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Aural.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Aural.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Aural.xcodeproj/project.xcworkspace/xcuserdata/dannier.xcuserdatad/UserInterfaceState.xcuserstate b/Aural.xcodeproj/project.xcworkspace/xcuserdata/dannier.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..42e4840b5732fda84360c205677800ed1d860027 GIT binary patch literal 20633 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 EABfF#3;+NC literal 0 HcmV?d00001 diff --git a/Aural.xcodeproj/xcuserdata/dannier.xcuserdatad/xcschemes/xcschememanagement.plist b/Aural.xcodeproj/xcuserdata/dannier.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..9ca7b3e --- /dev/null +++ b/Aural.xcodeproj/xcuserdata/dannier.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Aural.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Aural/Assets.xcassets/AccentColor.colorset/Contents.json b/Aural/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Aural/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Aural/Assets.xcassets/AppIcon.appiconset/Contents.json b/Aural/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..ffdfe15 --- /dev/null +++ b/Aural/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Aural/Assets.xcassets/Contents.json b/Aural/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Aural/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Aural/AudioRouteManager.swift b/Aural/AudioRouteManager.swift new file mode 100644 index 0000000..a50889b --- /dev/null +++ b/Aural/AudioRouteManager.swift @@ -0,0 +1,129 @@ +import Foundation +import AVFAudio +import Combine + +/// 管理系统音频会话与路由的单例类 +/// - 负责:配置 AVAudioSession、强制接管蓝牙输入(优先 AirPods)、监听路由变化并抛出状态 +@MainActor +final class AudioRouteManager: ObservableObject { + + static let shared = AudioRouteManager() + + enum RouteStatus { + case unknown + case bluetoothActive(deviceName: String?) + case notAvailable + } + + @Published private(set) var routeStatus: RouteStatus = .unknown + @Published private(set) var isBluetoothPreferredActive: Bool = false + @Published private(set) var currentInputName: String? + + private let session = AVAudioSession.sharedInstance() + private var notificationTokens: [NSObjectProtocol] = [] + + private init() { + configureSession() + startObservingRouteChanges() + refreshCurrentRoute() + } + + // MARK: - Public API + + /// 在需要开始录音前调用,确保会话被正确激活 + func activateSession() { + do { + try session.setActive(true, options: []) + refreshCurrentRoute() + } catch { + print("AudioRouteManager activateSession error: \(error)") + } + } + + /// 在录音结束或不再需要音频时调用 + func deactivateSession() { + do { + try session.setActive(false, options: [.notifyOthersOnDeactivation]) + } catch { + print("AudioRouteManager deactivateSession error: \(error)") + } + } + + /// 手动刷新当前路由状态 + func refreshCurrentRoute() { + updateRouteState(for: session.currentRoute) + } + + // MARK: - Private + + private func configureSession() { + do { + // .playAndRecord + measurement / spokenAudio,允许蓝牙 & A2DP + try session.setCategory( + .playAndRecord, + mode: .spokenAudio, + options: [ + .allowBluetooth, + .allowBluetoothA2DP + ] + ) + } catch { + print("AudioRouteManager configureSession error: \(error)") + } + } + + private func startObservingRouteChanges() { + let center = NotificationCenter.default + + let token = center.addObserver( + forName: AVAudioSession.routeChangeNotification, + object: session, + queue: .main + ) { [weak self] notification in + self?.handleRouteChange(notification: notification) + } + + notificationTokens.append(token) + } + + private func handleRouteChange(notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) else { + refreshCurrentRoute() + return + } + + switch reason { + case .newDeviceAvailable, .oldDeviceUnavailable, .routeConfigurationChange: + refreshCurrentRoute() + default: + break + } + } + + private func updateRouteState(for route: AVAudioSessionRouteDescription) { + let inputs = route.inputs + let activeInput = inputs.first + + currentInputName = activeInput?.portName + + if let portType = activeInput?.portType, isBluetoothPort(portType) { + isBluetoothPreferredActive = true + routeStatus = .bluetoothActive(deviceName: activeInput?.portName) + } else { + isBluetoothPreferredActive = false + routeStatus = inputs.isEmpty ? .notAvailable : .notAvailable + } + } + + private func isBluetoothPort(_ portType: AVAudioSession.Port) -> Bool { + switch portType { + case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE: + return true + default: + return false + } + } +} + diff --git a/Aural/Aural.entitlements b/Aural/Aural.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Aural/Aural.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Aural/AuralApp.swift b/Aural/AuralApp.swift new file mode 100644 index 0000000..20e9f63 --- /dev/null +++ b/Aural/AuralApp.swift @@ -0,0 +1,29 @@ +// +// AuralApp.swift +// Aural +// +// Created by 丹尼尔 on 2026/3/16. +// + +import SwiftUI +import SwiftData + +@main +struct AuralApp: App { + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + TranscriptSession.self, + TranscriptSegment.self + ]) + let configuration = ModelConfiguration() + return try! ModelContainer(for: schema, configurations: [configuration]) + }() + + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(sharedModelContainer) + } +} + diff --git a/Aural/ContentView.swift b/Aural/ContentView.swift new file mode 100644 index 0000000..85dc664 --- /dev/null +++ b/Aural/ContentView.swift @@ -0,0 +1,179 @@ +// +// ContentView.swift +// Aural +// +// Created by 丹尼尔 on 2026/3/16. +// + +import SwiftUI +import SwiftData + +struct ContentView: View { + @Environment(\.modelContext) private var modelContext + @Query(sort: \TranscriptSession.startTime, order: .reverse) + private var sessions: [TranscriptSession] + + @StateObject private var routeManager = AudioRouteManager.shared + @StateObject private var speechManager = SpeechRecognitionManager.shared + + @State private var isRecording: Bool = false + + var body: some View { + ZStack { + Color(.systemBackground) + .ignoresSafeArea() + + VStack(spacing: 32) { + statusBar + + Spacer() + + recordingButton + + scrollTranscriptView + .frame(maxHeight: .infinity, alignment: .top) + + Spacer(minLength: 32) + } + .padding(.horizontal, 32) + } + } + + private var recordingButton: some View { + let isBluetoothOK: Bool + switch routeManager.routeStatus { + case .bluetoothActive: + isBluetoothOK = true + default: + isBluetoothOK = false + } + + let disabled = !isBluetoothOK || speechManager.isRecognizing && !isRecording + + return Button { + Task { + if !isRecording { + // Start + let granted = await speechManager.requestAuthorization() + guard granted else { return } + + routeManager.activateSession() + + let deviceName = routeManager.currentInputName ?? "Unknown" + speechManager.attach(modelContext: modelContext) + speechManager.startSession(deviceName: deviceName) + } else { + // Stop + speechManager.stopSession() + routeManager.deactivateSession() + } + + withAnimation(.spring(response: 0.25, dampingFraction: 0.8)) { + isRecording.toggle() + } + } + } label: { + ZStack { + Circle() + .fill(disabled ? Color.gray.opacity(0.4) : (isRecording ? Color.red : Color.primary)) + .frame(width: 96, height: 96) + + Circle() + .strokeBorder(Color.primary.opacity(0.1), lineWidth: 4) + .frame(width: 120, height: 120) + + Image(systemName: isRecording ? "stop.fill" : "mic.fill") + .font(.system(size: 28, weight: .bold)) + .foregroundStyle(Color.white) + } + } + .buttonStyle(.plain) + .disabled(disabled) + .gesture( + LongPressGesture(minimumDuration: 0.7) + .onEnded { _ in + if isRecording { + speechManager.stopSession() + routeManager.deactivateSession() + withAnimation(.spring(response: 0.25, dampingFraction: 0.8)) { + isRecording = false + } + } + } + ) + .accessibilityLabel("Recording") + } + + private var scrollTranscriptView: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + if !speechManager.liveText.isEmpty { + Text(speechManager.liveText) + .font(.body) + .foregroundStyle(.primary) + .padding(.vertical, 4) + } + + if let latest = sessions.first { + ForEach(latest.segments, id: \.id) { segment in + Text(segment.text) + .font(.callout) + .foregroundStyle(.secondary) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 8) + } + } + + private var statusBar: some View { + HStack { + Circle() + .fill(statusColor) + .frame(width: 8, height: 8) + + if let error = speechManager.lastErrorDescription { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.yellow) + Text(error) + .font(.caption) + } else { + switch routeManager.routeStatus { + case .bluetoothActive(let name): + Text(name ?? "Bluetooth") + .font(.caption) + case .notAvailable: + Text("No Mic") + .font(.caption) + default: + EmptyView() + } + } + + Spacer() + } + .padding(.horizontal, 32) + .padding(.top, 12) + } + + private var statusColor: Color { + if speechManager.lastErrorDescription != nil { + return .red + } + switch routeManager.routeStatus { + case .bluetoothActive: + return .green + case .notAvailable: + return .gray + default: + return .orange + } + } +} + +#Preview { + ContentView() + .modelContainer(for: [TranscriptSession.self, TranscriptSegment.self], inMemory: true) +} + diff --git a/Aural/Preview Content/Preview Assets.xcassets/Contents.json b/Aural/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Aural/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Aural/SpeechRecognitionManager.swift b/Aural/SpeechRecognitionManager.swift new file mode 100644 index 0000000..5e13c0f --- /dev/null +++ b/Aural/SpeechRecognitionManager.swift @@ -0,0 +1,252 @@ +import Foundation +import AVFAudio +import Speech +import SwiftData + +/// 实时语音流式转写引擎 +/// - 使用 SFSpeechRecognizer + AVAudioEngine +/// - partial 结果只更新 UI +/// - final / 静音分句写入 SwiftData 的 TranscriptSession +@MainActor +final class SpeechRecognitionManager: ObservableObject { + + static let shared = SpeechRecognitionManager() + + // MARK: - 公共可绑定状态 + + /// 当前正在识别的文本(partial) + @Published private(set) var liveText: String = "" + /// 是否处于识别中 + @Published private(set) var isRecognizing: Bool = false + /// 最近一次出错信息(可用于极简提示) + @Published private(set) var lastErrorDescription: String? + + // MARK: - 私有属性 + + private let audioEngine = AVAudioEngine() + private var speechRecognizer: SFSpeechRecognizer? + private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? + private var recognitionTask: SFSpeechRecognitionTask? + + /// 静音分句相关 + private var lastResultDate: Date? + private var currentUtteranceBuffer: String = "" + + /// SwiftData 上下文,由外部注入 + var modelContext: ModelContext? + private var currentSession: TranscriptSession? + + private init() { + configureRecognizer() + } + + // MARK: - 配置 + + private func configureRecognizer(locale: Locale = Locale(identifier: Locale.current.identifier)) { + // 默认使用设备语言,若为中文优先 zh-CN + if Locale.current.language.languageCode?.identifier == "zh" { + speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "zh-CN")) + } else { + speechRecognizer = SFSpeechRecognizer(locale: locale) + } + } + + func attach(modelContext: ModelContext) { + self.modelContext = modelContext + } + + // MARK: - 权限 + + func requestAuthorization() async -> Bool { + let speechGranted = await withCheckedContinuation { (cont: CheckedContinuation) in + SFSpeechRecognizer.requestAuthorization { status in + cont.resume(returning: status == .authorized) + } + } + + let micGranted: Bool = await withCheckedContinuation { (cont: CheckedContinuation) in + AVAudioApplication.requestRecordPermission { granted in + cont.resume(returning: granted) + } + } + + if !speechGranted || !micGranted { + lastErrorDescription = "权限未授予" + } else { + lastErrorDescription = nil + } + + return speechGranted && micGranted + } + + // MARK: - Session 管理 + + func startSession(deviceName: String) { + guard !isRecognizing else { return } + guard let recognizer = speechRecognizer, recognizer.isAvailable else { + lastErrorDescription = "语音识别不可用" + return + } + guard let modelContext else { + lastErrorDescription = "数据上下文不可用" + return + } + + resetInternalState() + + let session = TranscriptSession( + startTime: Date(), + deviceConnected: deviceName, + segments: [] + ) + modelContext.insert(session) + currentSession = session + + setupRecognitionPipeline() + } + + func stopSession(commit: Bool = true) { + guard isRecognizing else { return } + + audioEngine.stop() + audioEngine.inputNode.removeTap(onBus: 0) + recognitionRequest?.endAudio() + recognitionTask?.cancel() + + isRecognizing = false + + if commit { + finalizeCurrentUtteranceIfNeeded() + if let currentSession { + currentSession.endTime = Date() + } + } else { + // 可选:丢弃当前 Session + } + } + + // MARK: - 管道搭建 + + private func setupRecognitionPipeline() { + let audioSession = AVAudioSession.sharedInstance() + + do { + try audioSession.setCategory(.record, mode: .measurement, options: [.allowBluetooth, .allowBluetoothA2DP]) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + lastErrorDescription = "音频会话配置失败" + return + } + + recognitionRequest = SFSpeechAudioBufferRecognitionRequest() + guard let recognitionRequest else { + lastErrorDescription = "创建请求失败" + return + } + + recognitionRequest.shouldReportPartialResults = true + + let inputNode = audioEngine.inputNode + let recordingFormat = inputNode.outputFormat(forBus: 0) + + inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in + guard let self, let recognitionRequest else { return } + recognitionRequest.append(buffer) + } + + audioEngine.prepare() + + do { + try audioEngine.start() + isRecognizing = true + lastErrorDescription = nil + } catch { + lastErrorDescription = "音频引擎启动失败" + return + } + + guard let recognizer = speechRecognizer else { + lastErrorDescription = "识别器未初始化" + return + } + + recognitionTask = recognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in + guard let self else { return } + + if let result = result { + handleRecognitionResult(result) + } + + if error != nil || (result?.isFinal ?? false) { + // 最后一次结果 + finalizeCurrentUtteranceIfNeeded() + self.isRecognizing = false + } + } + } + + // MARK: - 结果处理 & 分句 + + private func handleRecognitionResult(_ result: SFSpeechRecognitionResult) { + let text = result.bestTranscription.formattedString + liveText = text + lastResultDate = Date() + + if result.isFinal { + // Final 结果:立即固化为一个 Segment + appendSegment(with: text) + liveText = "" + currentUtteranceBuffer = "" + } else { + // Partial:只更新 UI,缓存在当前 utterance buffer 中 + currentUtteranceBuffer = text + } + } + + /// 根据静音(超过 2 秒无结果)进行分句 + func checkForSilenceAndFinalizeIfNeeded() { + guard let last = lastResultDate else { return } + let interval = Date().timeIntervalSince(last) + if interval > 2.0 { + finalizeCurrentUtteranceIfNeeded() + } + } + + private func finalizeCurrentUtteranceIfNeeded() { + let text = currentUtteranceBuffer.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { return } + + appendSegment(with: text) + currentUtteranceBuffer = "" + liveText = "" + } + + private func appendSegment(with text: String) { + guard let currentSession else { return } + + let segment = TranscriptSegment( + timestamp: Date(), + text: text + ) + currentSession.segments.append(segment) + } + + // MARK: - 辅助 + + private func resetInternalState() { + liveText = "" + lastErrorDescription = nil + lastResultDate = nil + currentUtteranceBuffer = "" + + recognitionTask?.cancel() + recognitionTask = nil + recognitionRequest = nil + + if audioEngine.isRunning { + audioEngine.stop() + audioEngine.inputNode.removeTap(onBus: 0) + } + } +} + diff --git a/Aural/TranscriptModels.swift b/Aural/TranscriptModels.swift new file mode 100644 index 0000000..842aee8 --- /dev/null +++ b/Aural/TranscriptModels.swift @@ -0,0 +1,40 @@ +import Foundation +import SwiftData + +@Model +final class TranscriptSegment { + var id: UUID + var timestamp: Date + var text: String + + init(id: UUID = UUID(), timestamp: Date = Date(), text: String) { + self.id = id + self.timestamp = timestamp + self.text = text + } +} + +@Model +final class TranscriptSession { + var id: UUID + var startTime: Date + var endTime: Date? + var deviceConnected: String + var segments: [TranscriptSegment] + + init( + id: UUID = UUID(), + startTime: Date = Date(), + endTime: Date? = nil, + deviceConnected: String, + segments: [TranscriptSegment] = [] + ) { + self.id = id + self.startTime = startTime + self.endTime = endTime + self.deviceConnected = deviceConnected + self.segments = segments + } +} + + diff --git a/AuralTests/AuralTests.swift b/AuralTests/AuralTests.swift new file mode 100644 index 0000000..3acd89a --- /dev/null +++ b/AuralTests/AuralTests.swift @@ -0,0 +1,16 @@ +// +// AuralTests.swift +// AuralTests +// +// Created by 丹尼尔 on 2026/3/16. +// + +import Testing + +struct AuralTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/AuralUITests/AuralUITests.swift b/AuralUITests/AuralUITests.swift new file mode 100644 index 0000000..18cb558 --- /dev/null +++ b/AuralUITests/AuralUITests.swift @@ -0,0 +1,43 @@ +// +// AuralUITests.swift +// AuralUITests +// +// Created by 丹尼尔 on 2026/3/16. +// + +import XCTest + +final class AuralUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/AuralUITests/AuralUITestsLaunchTests.swift b/AuralUITests/AuralUITestsLaunchTests.swift new file mode 100644 index 0000000..3341380 --- /dev/null +++ b/AuralUITests/AuralUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// AuralUITestsLaunchTests.swift +// AuralUITests +// +// Created by 丹尼尔 on 2026/3/16. +// + +import XCTest + +final class AuralUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/README.md b/README.md index e69de29..aa33d74 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,68 @@ +第一步:项目初始化与全局数据流设计 (发给 Agent 的 Prompt 1) +Role & Context: 你现在是一个资深的 iOS 架构师。我们需要开发一个名为 Airtep Voice 的 iOS POC 应用。这个应用的核心功能是:连接 AirPods,实时采集会议或谈话的语音,并利用苹果原生框架进行流式转写,最后将文本结构化落盘。 + +任务 1:项目初始化与 UI 规范 + +请创建一个基于 SwiftUI 和 SwiftData 的项目。 + +在 Info.plist 中添加必要的权限描述:NSMicrophoneUsageDescription (用于环境录音) 和 NSSpeechRecognitionUsageDescription (用于实时语音转文字)。 + +UI 设计规范(极高优先级):在视觉呈现上,请严格遵循极简主义和对称美学,深度参考 Apple 的原生设计原则。所有的图标和界面元素必须采用纯色方案,绝对不要使用渐变色。界面需尽量去除多余的辅助性文字,依靠几何对称和留白来引导用户操作。主界面只需要一个居中的、对称的录音控制按钮(纯色圆形或圆角矩形),以及下方一个平滑展开的滚动文本视图。 + +任务 2:数据架构设计 +为了让产生的数据在未来具备良好的“可观测、可对齐、可索引”特性,请使用 SwiftData 定义一个 TranscriptSession 模型。 +包含字段: + +id: UUID + +startTime: Date + +endTime: Date? + +deviceConnected: String (记录当前使用的音频外设名称) + +segments: [TranscriptSegment] (嵌套模型,记录每一句话的 timestamp 和 text,便于后续切片和对齐) + +请输出初始化的工程结构建议、SwiftData 模型代码以及极简风格的主视图结构。 + +第二步:核心音频路由与蓝牙设备强制接管 (发给 Agent 的 Prompt 2) +任务 3:开发 AVAudioSession 路由管理器 +现在我们需要编写底层的音频控制模块 AudioRouteManager.swift。 + +配置 AVAudioSession:设置 category 为 .playAndRecord,mode 为 .measurement 或 .spokenAudio。 + +关键选项:必须包含 .allowBluetooth 和 .allowBluetoothA2DP,确保系统能强制从用户的蓝牙耳机(AirPods)获取音频输入,而不是默认的手机麦克风。 + +编写一个状态监听器,当系统音频路由发生变化(例如用户摘下 AirPods)时,能够实时抛出状态变更,UI 需根据这个状态将纯色录音按钮置灰或改变形状。 + +请提供这个单例管理类的完整 Swift 代码。 + +第三步:实时语音流式转文字引擎 (发给 Agent 的 Prompt 3) +任务 4:开发 SFSpeechRecognizer 流式处理引擎 +我们需要实现 SpeechRecognitionManager.swift。 + +实例化 SFSpeechRecognizer,并确保 locale 设置为当前设备语言(默认中文 zh-CN)。 + +使用 AVAudioEngine 捕获刚才配置好的音频输入节点的 buffer。 + +创建 SFSpeechAudioBufferRecognitionRequest,将 shouldReportPartialResults 设为 true。 + +在 installTap 的回调中,将音频流实时喂给识别请求。 + +结构化处理:当转写回调触发时,不要只拼接长文本。如果是中间结果(partial),只更新当前 UI;如果是最终结果(或者根据静音停顿超过 2 秒作为分句逻辑),将这段话封装为一个 TranscriptSegment 对象,对齐时间戳,并追加到 SwiftData 的当前 Session 中,保证所有数据都是结构化且可索引的。 + +请输出包含上述逻辑的完整类实现,并确保它是一个 ObservableObject,以便 SwiftUI 视图实时绑定数据。 + +第四步:界面组装与交互联动 (发给 Agent 的 Prompt 4) +任务 5:组装 POC 完整流程 +结合前面写好的 AudioRouteManager 和 SpeechRecognitionManager,以及极简的 SwiftUI 视图,把它们串联起来。 + +当用户点击屏幕正中央的纯色对称按钮时,请求权限 -> 检查 AirPods 连接状态 -> 启动 AudioEngine -> 开始流式转写。 + +在按钮下方,实时渲染当前正在说的话。 + +提供一个手势(如向下轻扫或长按按钮)来停止 Session,并将这一整段结构化数据正式持久化到本地。 + +处理可能出现的异常边界(如未授权、未连接麦克风),用极简的图标震动或纯色状态条进行提示,避免长篇大论的弹窗文字。 + +请输出整合后的 ContentView.swift 和应用入口文件,确保这份代码丢进 Xcode 就能直接编译运行。 \ No newline at end of file