Compare commits

..

20 Commits

Author SHA1 Message Date
Max ae3a951950 Merge branch 'release/1.0.0' 2025-09-09 17:22:56 +08:00
Max 508244fac3 Remove unused confidence and alternatives fields from SpeechRecognitionResult
Complete removal of unused fields to simplify data structure:

1. SpeechRecognitionResult Model Simplified:
   - Removed 'confidence' field (was always hardcoded to 0.8)
   - Removed 'alternatives' field (was always empty array)
   - Kept only 'recognizedWords' which is actually used
   - Updated constructor, fromMap, toMap, toString, ==, hashCode accordingly

2. YxAsrService Updates:
   - Simplified _sendResult() method signature
   - Removed unused confidence and alternatives parameters
   - Updated method call to only pass recognizedWords
   - Cleaner method invocation: _sendResult(recognizedWords: result.text)

3. Benefits Achieved:
   - 🧹 Simplified data structure - only essential fields remain
   - 🚀 Reduced memory usage - no unnecessary field storage/transmission
   - 💡 Cleaner API - method signatures reflect actual usage
   -  Better performance - less data serialization/deserialization
   - 🔍 Improved code clarity - no confusing unused parameters

4. Sherpa-ONNX Integration:
   - OnlineRecognizerResult only provides: text, tokens, timestamps
   - No confidence or alternatives data available from the library
   - Our simplified structure now aligns with actual data source

This optimization removes all the 'fake' hardcoded values and focuses
on the actual speech recognition text result that users need.
2025-09-09 17:03:43 +08:00
Max 75080d0c0d Fix duplicate recognition issue in _recognitionTimer
Critical fix for recognition logic to prevent duplicate processing:

1. Problem Identified:
   - _recognitionTimer was repeatedly calling decode() on same audio data
   - Same recognition results were being sent multiple times to UI
   - Caused redundant processing and potential performance issues

2. Solution Implemented:
   - Add _lastRecognizedText state variable to track previous results
   - Only send recognition results when text content actually changes
   - Reset _lastRecognizedText when starting new recording session

3. Logic Changes:
   - Enhanced recognition loop with duplicate detection:
     A command-line utility for Dart development.

Usage: dart <command|dart-file> [arguments]

Global options:
-v, --verbose               Show additional command output.
    --version               Print the Dart SDK version.
    --enable-analytics      Enable analytics.
    --disable-analytics     Disable analytics.
    --suppress-analytics    Disallow analytics for this `dart *` run without changing the analytics configuration.
-h, --help                  Print this usage information.

Available commands:
  analyze    Analyze Dart code in a directory.
  compile    Compile Dart to various formats.
  create     Create a new Dart project.
  devtools   Open DevTools (optionally connecting to an existing application).
  doc        Generate API documentation for Dart projects.
  fix        Apply automated fixes to Dart source code.
  format     Idiomatically format Dart source code.
  info       Show diagnostic information about the installed tooling.
  pub        Work with packages.
  run        Run a Dart program.
  test       Run tests for a project.

Run "dart help <command>" for more information about a command.
See https://dart.dev/tools/dart-tool for detailed documentation.
   - Added debug logging for skipped duplicate results
   - Reset state on startListening() to ensure clean slate

4. Benefits:
   - Eliminates duplicate recognition results sent to UI
   - Reduces unnecessary computation and network overhead
   - Improves user experience with cleaner, non-repetitive updates
   - Better resource utilization and battery life

This fix addresses the core issue where the recognition timer was
processing the same audio stream content repeatedly, ensuring each
unique recognition result is only sent once to the application.
2025-09-09 16:58:47 +08:00
Max e961996ec6 Remove recognition history functionality
Complete removal of recognition history features:

1. State Variables Removed:
   - _recognitionHistory list variable
   - _realtimeResults list variable
   - All history-related state management

2. Methods Updated:
   - _clearHistory() renamed to _clearContent()
   - Simplified to only clear current text and base text
   - Removed history list operations

3. UI Components Removed:
   - _buildHistoryCard() method completely removed
   - History card from main layout removed
   - History-related ListView and ListTile widgets removed

4. Callback Logic Simplified:
   - onListeningStatusChanged callback cleaned up
   - Removed history insertion logic when recording stops
   - Removed _realtimeResults.clear() operations
   - Simplified state management to focus only on current text

5. App Bar Updated:
   - Clear button tooltip changed from '清除历史' to '清除内容'
   - Button now calls _clearContent() instead of _clearHistory()

The app now focuses purely on real-time speech recognition with
editable text input, without maintaining any recognition history.
This simplifies the codebase and improves performance by removing
unnecessary data storage and UI rendering.
2025-09-09 16:22:38 +08:00
Max aba8b44ab8 Update RecordingButton design with new visual style
Major design changes based on user requirements:

1. Background Style Changes:
   - Remove solid color background and shadows
   - Use semi-transparent background (alpha: 0.12) for subtle visual feedback
   - Maintain circular shape for both states

2. Icon Improvements:
   - Increase icon size to 55% of button size for better visibility
   - Use color-coded icons: blue for idle, red for recording
   - Remove white icon color, use theme-based colors instead

3. Loading Indicator Enhancement:
   - Show CircularProgressIndicator during both processing and listening states
   - Position indicator as overlay using Stack layout
   - Maintain white color for progress indicator for contrast

4. Color Logic Simplification:
   - Consolidate color logic into single iconColor variable
   - Remove complex state-based color transitions
   - Use consistent color scheme: blue (#2196F3) idle, red (#FF5252) recording
   - Darker disabled color (#212121) for better contrast

5. Layout Structure Update:
   - Replace Transform.scale animation wrapper with direct Container
   - Use Stack for layering icon and progress indicator
   - Positioned widgets for precise control over element placement

This creates a more modern, minimalist design with better visual hierarchy
and improved accessibility through higher contrast ratios.
2025-09-09 15:08:35 +08:00
Max ac234d99ec Fix RecordingButton shape: Both states should be circular
Correction based on user feedback:
- Both idle and recording states use circular shape (BoxShape.circle)
- Remove conditional rectangle shape logic
- Keep color differentiation: blue for idle, red for recording
- Maintain consistent circular InkWell ripple effect

Design now correctly matches the mockup:
🔵 Blue circle (idle/ready to record)
🔴 Red circle (actively recording)

Both buttons maintain the same circular shape with only color changes.
2025-09-09 14:23:47 +08:00
Max 3e8fe73e27 Update RecordingButton design to match UI mockup
Design improvements based on provided mockup:

1. Color Updates:
   - Idle state: Blue (#2196F3) - matches mockup's blue recording button
   - Recording state: Red (#FF5252) - matches mockup's red stop button
   - Disabled state: Light grey (#9E9E9E) for better visibility
   - Processing state: Semi-transparent versions of respective colors

2. Shape Changes:
   - Idle state: Circle (for recording button)
   - Recording state: Rounded rectangle (12px radius) for stop button
   - Smooth transitions between shapes using BoxShape

3. Icon Improvements:
   - Use rounded icons (mic_rounded, stop_rounded) for modern look
   - Adjust icon sizes: recording (40% of button), stop (35% of button)
   - Better proportions matching the design intent

4. Shadow Enhancements:
   - Increased blur radius for idle state (8→12px) for better depth
   - Dynamic shadow based on button state
   - Enhanced spread radius for better visual impact

5. Interactive Elements:
   - InkWell borderRadius adapts to button shape
   - Smooth transitions between circular and rectangular ripple effects

These changes create a modern, professional appearance that matches
common design patterns and provides clear visual feedback for different states.
2025-09-09 14:20:10 +08:00
Max 6146508bed Enhance RecordingButton with haptic feedback and debounce
Major UX improvements to RecordingButton:

1. Haptic Feedback (震动效果):
   - Add HapticFeedback.lightImpact() on button tap
   - Import flutter/services.dart for haptic support
   - Provides tactile confirmation for user interactions

2. Debounce Mechanism (防抖):
   - Add _isProcessing flag to prevent rapid consecutive taps
   - 300ms cooldown period after each tap
   - Protects against accidental double-taps and ensures stable operation

3. Enhanced Visual States:
   - Processing state with semi-transparent color
   - Loading spinner during async operations
   - Better visual feedback for different button states

4. Improved Animations:
   - Trigger scale animation on tap (not just on state change)
   - Smooth forward/reverse animation cycle
   - Better visual response to user interactions

5. Better Error Handling:
   - Proper mounted check before setState
   - Graceful cleanup with finally block
   - Prevents memory leaks and state corruption

These improvements provide:
- Better tactile feedback for users
- Prevention of UI race conditions
- Clearer visual indication of button states
- More responsive and professional user experience
2025-09-09 14:06:43 +08:00
Max 0af37c5b87 Remove final result logic from SDK library
Major SDK simplification by removing redundant final result processing:

1. YxAsrService changes:
   - Remove final result retrieval in stopListening()
   - Remove finalResult parameter from _sendResult()
   - Simplify stop logic to only reset stream state
   - Eliminate duplicate API calls that provided no additional value

2. SpeechRecognitionResult model changes:
   - Remove finalResult property and related logic
   - Update constructor, factory methods, toString, equals, hashCode
   - Remove finalResult from toMap/fromMap serialization
   - Simplify the model to focus on actual recognition data

3. Benefits:
   - Cleaner, more maintainable codebase
   - Reduced complexity and potential bugs
   - Better performance (no redundant API calls)
   - Simpler API for developers to use
   - Real-time text appending works seamlessly without artificial distinctions

The analysis showed that 'final results' were identical to the last real-time result,
making the distinction unnecessary. Now all results are treated uniformly as
real-time updates, providing a smoother and more intuitive user experience.
2025-09-09 11:29:19 +08:00
Max ed51fa89bd Implement real-time text appending to input field
1. Add _baseText variable to track text before recording starts
   - Preserves existing text when starting new recording session
   - Provides foundation for real-time appending

2. Update real-time result processing logic
   - Real-time results now immediately update the text field
   - Combine base text + current recognition text
   - Move cursor to end for better UX
   - Add intelligent spacing between segments

3. Improve final result handling
   - Final results update base text for next recording
   - Clean state management between recording sessions

4. Update recording status change logic
   - Capture base text when recording starts
   - Maintain proper state transitions

Now users can see their speech being converted to text in real-time directly in the input field, providing immediate visual feedback during speech recognition.
2025-09-09 11:24:53 +08:00
Max d1ab67e60e Fix real-time speech recognition display
1. Update UI to show actual _currentText instead of fixed 'realtime recognizing...' text
   - Use Flexible widget to handle text overflow
   - Show actual recognition text when available
   - Fallback to 'realtime recognizing...' when text is empty

2. Fix final result processing logic
   - Don't update _currentText when processing final results
   - Final results only append to text field, don't interfere with real-time display

3. Improve listening status management
   - Clear _currentText when recording stops
   - Ensure clean state transitions between recording sessions

This fixes the issue where users could only see final results without real-time feedback.
2025-09-09 11:22:37 +08:00
Max d63124203b Fix critical bugs in speech recognition system
1. Add finalResult property to SpeechRecognitionResult class
   - Distinguish between real-time and final recognition results
   - Update factory methods, toString, equals, and hashCode
   - Update toMap and fromMap methods

2. Update YxAsrService to support finalResult flag
   - Add finalResult parameter to _sendResult method
   - Mark final results with finalResult: true
   - Keep real-time results as finalResult: false (default)

3. Remove unused methods to clean up codebase
   - Remove unused _toggleRecording method
   - Remove unused _updateTextController method
   - Clean up orphaned comments

These fixes resolve linter errors and ensure proper text appending functionality.
2025-09-09 11:17:13 +08:00
Max ea32370fcc Improve text input behavior: append recognition results to text field
- Change from replacing current text to appending final results
- Distinguish between real-time results (display only) and final results (append to text)
- Add proper spacing between appended text segments
- Maintain history of individual recognition segments
- Clear current text when starting new recording session
2025-09-09 11:08:44 +08:00
Max 36fa796c10 Improve demo: use RecordingButton component instead of custom implementation
- Replace custom FloatingActionButton with RecordingButton widget
- Simplify code by using built-in speech recognition integration
- Demonstrate proper usage of RecordingButton component
- Reduce demo code complexity from 80+ lines to ~40 lines
- Better showcase the plugin's provided components
2025-09-09 11:04:27 +08:00
Max c47ad5c435 Fix Flutter assets configuration: explicitly specify all model files in pubspec.yaml 2025-09-06 17:36:54 +08:00
Max d18c3fc607 Switch back to zh2023 model to fix filename mismatch issue 2025-09-06 17:30:54 +08:00
Max 9053132874 Fix model loading: switch to zh2025 model as recommended by logs 2025-09-06 17:29:11 +08:00
Max 58056c6c09 Fix Kotlin null safety issue: add null check for context in isAvailable method 2025-09-06 17:26:24 +08:00
Max 80d99d6bdd Fix Android namespace issues: add namespace to build.gradle, update AGP and Kotlin versions 2025-09-06 17:25:05 +08:00
Max ae1ec191a8 Fix build issues: remove duplicate gradle files, increase heap size, fix plugin configuration 2025-09-06 17:23:14 +08:00
51 changed files with 277 additions and 7086 deletions

View File

@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@ -1 +0,0 @@
{"version":2,"entries":[{"package":"yx_asr","rootUri":"../","packageUri":"lib/"}]}

View File

@ -1 +0,0 @@
{"version":2,"entries":[{"package":"yx_asr","rootUri":"../","packageUri":"lib/"}]}

View File

@ -1,32 +0,0 @@
//
// Generated file. Do not edit.
// This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`.
//
// @dart = 3.0
import 'dart:io'; // flutter_ignore: dart_io_import.
import 'package:record_linux/record_linux.dart';
@pragma('vm:entry-point')
class _PluginRegistrant {
@pragma('vm:entry-point')
static void register() {
if (Platform.isAndroid) {
} else if (Platform.isIOS) {
} else if (Platform.isLinux) {
try {
RecordLinux.registerWith();
} catch (err) {
print(
'`record_linux` threw an error: $err. '
'The app may not function as expected until you remove this plugin from pubspec.yaml'
);
}
} else if (Platform.isMacOS) {
} else if (Platform.isWindows) {
}
}
}

View File

@ -252,8 +252,8 @@ file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/lib/
yx_asr
3.0
file:///Users/max/SourceCode/yuanxuan/yx_asr/
file:///Users/max/SourceCode/yuanxuan/yx_asr/lib/
file:///Users/max/SourceCode/yuanxuan/yx_speech_to_text_flutter/
file:///Users/max/SourceCode/yuanxuan/yx_speech_to_text_flutter/lib/
sky_engine
3.7
file:///Users/max/fvm/versions/3.32.0/bin/cache/pkg/sky_engine/

View File

@ -88,6 +88,18 @@
"vm_service"
]
},
{
"name": "path_provider",
"version": "2.1.5",
"dependencies": [
"flutter",
"path_provider_android",
"path_provider_foundation",
"path_provider_linux",
"path_provider_platform_interface",
"path_provider_windows"
]
},
{
"name": "record",
"version": "6.1.1",
@ -361,6 +373,52 @@
"webdriver"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "path_provider_foundation",
"version": "2.4.2",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_android",
"version": "2.2.17",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "record_macos",
"version": "1.1.1",
@ -543,6 +601,21 @@
"vm_service"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "web",
"version": "1.1.1",
@ -560,13 +633,6 @@
"vector_math"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "fixnum",
"version": "1.1.1",
@ -590,72 +656,6 @@
"dependencies": [
"collection"
]
},
{
"name": "path_provider",
"version": "2.1.5",
"dependencies": [
"flutter",
"path_provider_android",
"path_provider_foundation",
"path_provider_linux",
"path_provider_platform_interface",
"path_provider_windows"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path_provider_foundation",
"version": "2.4.2",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "path_provider_android",
"version": "2.2.17",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
}
],
"configVersion": 1

File diff suppressed because one or more lines are too long

View File

@ -2,14 +2,14 @@ group 'com.yuanxuan.yx_asr'
version '1.0.0'
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -25,6 +25,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace 'com.yuanxuan.yx_asr'
compileSdkVersion 33
compileOptions {

View File

@ -81,7 +81,9 @@ class YxAsrPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegist
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"isAvailable" -> {
result.success(SpeechRecognizer.isRecognitionAvailable(context))
context?.let {
result.success(SpeechRecognizer.isRecognitionAvailable(it))
} ?: result.success(false)
}
"hasPermission" -> {
result.success(hasPermission())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987f5b3155b7b26c88d68696c21f46cc3e","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/record_ios","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"record_ios","INFOPLIST_FILE":"Target Support Files/record_ios/ResourceBundle-record_ios_privacy-record_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"record_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e986f06ec316eb44a4c4da4527cd08c3a8f","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98bfb1d663e62fa3d6f40293efaffb810f","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/record_ios","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"record_ios","INFOPLIST_FILE":"Target Support Files/record_ios/ResourceBundle-record_ios_privacy-record_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"record_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98d3a7bc21be11eea36650c14a981a4a21","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98bfb1d663e62fa3d6f40293efaffb810f","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/record_ios","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"record_ios","INFOPLIST_FILE":"Target Support Files/record_ios/ResourceBundle-record_ios_privacy-record_ios-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"record_ios_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9893f6c2ca2ac7ceb884c507476cc3a3a1","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e987eba38ad46157cacab6baf46e2c97b28","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98585c515449a114c265106ae35ef78056","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98b18105efd5bafedefc5647c8499985e6","guid":"bfdfe7dc352907fc980b868725387e98a02cab793f6ffe791c8a532f4724d833"}],"guid":"bfdfe7dc352907fc980b868725387e988ccaa3aab9ec360ff6e16378731b23fa","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e982a2ee81fc4f9376a4b6bb6d6bb502a00","name":"record_ios-record_ios_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98c34601a2dc07dcfea6b09ca49bc4da60","name":"record_ios_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981397e4ca66b8bc1b6b787af196ee2a0c","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98d769a3a3853c9249977754ea24f3b8bb","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98201acde82baf32765e719536c1fcd415","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e989a076473f270b5d52145183a44aba102","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98201acde82baf32765e719536c1fcd415","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/permission_handler_apple","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"permission_handler_apple","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"permission_handler_apple_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98682380182db58e4ded87336dc6e1d894","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98c26bdf2e10ce31dd6b04911f1aadd468","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98cf73538f434be7a8ded6885071e43bbb","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e981993b5887705d08bd566da0ef34911dc","guid":"bfdfe7dc352907fc980b868725387e9830ed596f4e641fbb73e74f191ee1d3f3"}],"guid":"bfdfe7dc352907fc980b868725387e98966a90ee2025d7afde1b9bd8d4be3432","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9802f35ab680609a626ebd2ddd692a3822","name":"permission_handler_apple-permission_handler_apple_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e983e9a904e8a35cb34b69458780be142b3","name":"permission_handler_apple_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9881514ee24132a163c8de1269d399c3bc","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e980f1ae418d2bbf8ce0c0848ff9e9d99f1","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98aed8c561cc665a91c70adde791d8e192","buildSettings":{"CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98fbefabc0b715bbc6bf42d5c546a088eb","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98aed8c561cc665a91c70adde791d8e192","buildSettings":{"CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","ENABLE_BITCODE":"NO","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"13.0","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98b6b192a2f6d1562adae81a957dd2959b","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98cb74bc54605fe2f3d0ea9f0289a8b241","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9881eaa5d02bc8b82d2651fdd2f994e058","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9817004907bf7c7d94734b13afa4b26c8b","guid":"bfdfe7dc352907fc980b868725387e985e64de23d5b31c171c50ec6da5267645"}],"guid":"bfdfe7dc352907fc980b868725387e98c72ecd8a391cdbdaacff87f6f0d76da8","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986e649604f74c414a7c2dbe5ef4cc4e75","name":"path_provider_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e983b4389e41a26a4fc852e2feb08c32b31","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_BITCODE":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e982cf0da236cf10d087750aa1434da9227","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e980eb0dfc45486a9a195a5a19657d4bdde","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_BITCODE":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98cc28f154213fd8181aa70d4c188a8335","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e980eb0dfc45486a9a195a5a19657d4bdde","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ENABLE_OBJC_WEAK":"NO","ENABLE_BITCODE":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1","IPHONEOS_DEPLOYMENT_TARGET":"13.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","OTHER_LDFLAGS":"$(inherited) -framework AudioToolbox -framework AVFoundation -framework Speech","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e981f19fefc6e52ad9e4e005a2248234387","name":"Release"}],"buildPhases":[],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"}

View File

@ -0,0 +1 @@
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/max/SourceCode/yuanxuan/yx_speech_to_text_flutter/example/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=7f6a4f64a6e2fd3720afe00c8dd0402f_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}

View File

@ -1 +0,0 @@
{"format-version":[1,0,0],"native-assets":{}}

View File

@ -1 +0,0 @@
assets/models/README.md  assetassets/models/README.md.assets/models/decoder-epoch-99-avg-1.int8.onnx  asset.assets/models/decoder-epoch-99-avg-1.int8.onnx.assets/models/encoder-epoch-99-avg-1.int8.onnx  asset.assets/models/encoder-epoch-99-avg-1.int8.onnx-assets/models/joiner-epoch-99-avg-1.int8.onnx  asset-assets/models/joiner-epoch-99-avg-1.int8.onnxassets/models/tokens.txt  assetassets/models/tokens.txt7packages/record_web/assets/js/record.fixwebmduration.js  asset7packages/record_web/assets/js/record.fixwebmduration.js/packages/record_web/assets/js/record.worklet.js  asset/packages/record_web/assets/js/record.worklet.js

View File

@ -1 +0,0 @@
{"assets/models/README.md":["assets/models/README.md"],"assets/models/decoder-epoch-99-avg-1.int8.onnx":["assets/models/decoder-epoch-99-avg-1.int8.onnx"],"assets/models/encoder-epoch-99-avg-1.int8.onnx":["assets/models/encoder-epoch-99-avg-1.int8.onnx"],"assets/models/joiner-epoch-99-avg-1.int8.onnx":["assets/models/joiner-epoch-99-avg-1.int8.onnx"],"assets/models/tokens.txt":["assets/models/tokens.txt"],"packages/record_web/assets/js/record.fixwebmduration.js":["packages/record_web/assets/js/record.fixwebmduration.js"],"packages/record_web/assets/js/record.worklet.js":["packages/record_web/assets/js/record.worklet.js"]}

View File

@ -1 +0,0 @@
[]

Binary file not shown.

View File

@ -1 +0,0 @@
{"format-version":[1,0,0],"native-assets":{}}

View File

@ -1,56 +0,0 @@
# 语音识别模型文件
这个目录包含了 sherpa_onnx 语音识别所需的模型文件。
## 模型结构
```
assets/models/
├── zh-cn/ # 中文模型
│ ├── encoder.onnx # 编码器模型
│ ├── decoder.onnx # 解码器模型
│ ├── joiner.onnx # 连接器模型
│ └── tokens.txt # 词汇表
├── en-us/ # 英文模型
│ ├── encoder.onnx
│ ├── decoder.onnx
│ ├── joiner.onnx
│ └── tokens.txt
└── multilingual/ # 多语言模型
├── encoder.onnx
├── decoder.onnx
├── joiner.onnx
└── tokens.txt
```
## 模型下载
由于模型文件较大,请从以下地址下载对应的模型文件:
### 中文模型 (推荐)
- 模型名称: sherpa-onnx-streaming-zipformer-bilingual-zh-en-2023-02-20
- 下载地址: https://github.com/k2-fsa/sherpa-onnx/releases/
- 大小: ~40MB
### 英文模型
- 模型名称: sherpa-onnx-streaming-zipformer-en-2023-02-21
- 下载地址: https://github.com/k2-fsa/sherpa-onnx/releases/
- 大小: ~40MB
### 多语言模型
- 模型名称: sherpa-onnx-streaming-zipformer-multilingual-2023-02-20
- 下载地址: https://github.com/k2-fsa/sherpa-onnx/releases/
- 大小: ~60MB
## 使用说明
1. 下载对应的模型文件
2. 解压到对应的语言目录
3. 确保文件名和路径正确
4. 重新构建应用
## 注意事项
- 模型文件会增加应用包大小
- 建议根据需要只包含必要的语言模型
- 模型文件支持热更新,可以在运行时下载

File diff suppressed because it is too large Load Diff

View File

@ -1,507 +0,0 @@
(function (name, definition) {
window.jsFixWebmDuration = definition();
})('fix-webm-duration', function () {
/*
* This is the list of possible WEBM file sections by their IDs.
* Possible types: Container, Binary, Uint, Int, String, Float, Date
*/
var sections = {
0xa45dfa3: { name: 'EBML', type: 'Container' },
0x286: { name: 'EBMLVersion', type: 'Uint' },
0x2f7: { name: 'EBMLReadVersion', type: 'Uint' },
0x2f2: { name: 'EBMLMaxIDLength', type: 'Uint' },
0x2f3: { name: 'EBMLMaxSizeLength', type: 'Uint' },
0x282: { name: 'DocType', type: 'String' },
0x287: { name: 'DocTypeVersion', type: 'Uint' },
0x285: { name: 'DocTypeReadVersion', type: 'Uint' },
0x6c: { name: 'Void', type: 'Binary' },
0x3f: { name: 'CRC-32', type: 'Binary' },
0xb538667: { name: 'SignatureSlot', type: 'Container' },
0x3e8a: { name: 'SignatureAlgo', type: 'Uint' },
0x3e9a: { name: 'SignatureHash', type: 'Uint' },
0x3ea5: { name: 'SignaturePublicKey', type: 'Binary' },
0x3eb5: { name: 'Signature', type: 'Binary' },
0x3e5b: { name: 'SignatureElements', type: 'Container' },
0x3e7b: { name: 'SignatureElementList', type: 'Container' },
0x2532: { name: 'SignedElement', type: 'Binary' },
0x8538067: { name: 'Segment', type: 'Container' },
0x14d9b74: { name: 'SeekHead', type: 'Container' },
0xdbb: { name: 'Seek', type: 'Container' },
0x13ab: { name: 'SeekID', type: 'Binary' },
0x13ac: { name: 'SeekPosition', type: 'Uint' },
0x549a966: { name: 'Info', type: 'Container' },
0x33a4: { name: 'SegmentUID', type: 'Binary' },
0x3384: { name: 'SegmentFilename', type: 'String' },
0x1cb923: { name: 'PrevUID', type: 'Binary' },
0x1c83ab: { name: 'PrevFilename', type: 'String' },
0x1eb923: { name: 'NextUID', type: 'Binary' },
0x1e83bb: { name: 'NextFilename', type: 'String' },
0x444: { name: 'SegmentFamily', type: 'Binary' },
0x2924: { name: 'ChapterTranslate', type: 'Container' },
0x29fc: { name: 'ChapterTranslateEditionUID', type: 'Uint' },
0x29bf: { name: 'ChapterTranslateCodec', type: 'Uint' },
0x29a5: { name: 'ChapterTranslateID', type: 'Binary' },
0xad7b1: { name: 'TimecodeScale', type: 'Uint' },
0x489: { name: 'Duration', type: 'Float' },
0x461: { name: 'DateUTC', type: 'Date' },
0x3ba9: { name: 'Title', type: 'String' },
0xd80: { name: 'MuxingApp', type: 'String' },
0x1741: { name: 'WritingApp', type: 'String' },
// 0xf43b675: { name: 'Cluster', type: 'Container' },
0x67: { name: 'Timecode', type: 'Uint' },
0x1854: { name: 'SilentTracks', type: 'Container' },
0x18d7: { name: 'SilentTrackNumber', type: 'Uint' },
0x27: { name: 'Position', type: 'Uint' },
0x2b: { name: 'PrevSize', type: 'Uint' },
0x23: { name: 'SimpleBlock', type: 'Binary' },
0x20: { name: 'BlockGroup', type: 'Container' },
0x21: { name: 'Block', type: 'Binary' },
0x22: { name: 'BlockVirtual', type: 'Binary' },
0x35a1: { name: 'BlockAdditions', type: 'Container' },
0x26: { name: 'BlockMore', type: 'Container' },
0x6e: { name: 'BlockAddID', type: 'Uint' },
0x25: { name: 'BlockAdditional', type: 'Binary' },
0x1b: { name: 'BlockDuration', type: 'Uint' },
0x7a: { name: 'ReferencePriority', type: 'Uint' },
0x7b: { name: 'ReferenceBlock', type: 'Int' },
0x7d: { name: 'ReferenceVirtual', type: 'Int' },
0x24: { name: 'CodecState', type: 'Binary' },
0x35a2: { name: 'DiscardPadding', type: 'Int' },
0xe: { name: 'Slices', type: 'Container' },
0x68: { name: 'TimeSlice', type: 'Container' },
0x4c: { name: 'LaceNumber', type: 'Uint' },
0x4d: { name: 'FrameNumber', type: 'Uint' },
0x4b: { name: 'BlockAdditionID', type: 'Uint' },
0x4e: { name: 'Delay', type: 'Uint' },
0x4f: { name: 'SliceDuration', type: 'Uint' },
0x48: { name: 'ReferenceFrame', type: 'Container' },
0x49: { name: 'ReferenceOffset', type: 'Uint' },
0x4a: { name: 'ReferenceTimeCode', type: 'Uint' },
0x2f: { name: 'EncryptedBlock', type: 'Binary' },
0x654ae6b: { name: 'Tracks', type: 'Container' },
0x2e: { name: 'TrackEntry', type: 'Container' },
0x57: { name: 'TrackNumber', type: 'Uint' },
0x33c5: { name: 'TrackUID', type: 'Uint' },
0x3: { name: 'TrackType', type: 'Uint' },
0x39: { name: 'FlagEnabled', type: 'Uint' },
0x8: { name: 'FlagDefault', type: 'Uint' },
0x15aa: { name: 'FlagForced', type: 'Uint' },
0x1c: { name: 'FlagLacing', type: 'Uint' },
0x2de7: { name: 'MinCache', type: 'Uint' },
0x2df8: { name: 'MaxCache', type: 'Uint' },
0x3e383: { name: 'DefaultDuration', type: 'Uint' },
0x34e7a: { name: 'DefaultDecodedFieldDuration', type: 'Uint' },
0x3314f: { name: 'TrackTimecodeScale', type: 'Float' },
0x137f: { name: 'TrackOffset', type: 'Int' },
0x15ee: { name: 'MaxBlockAdditionID', type: 'Uint' },
0x136e: { name: 'Name', type: 'String' },
0x2b59c: { name: 'Language', type: 'String' },
0x6: { name: 'CodecID', type: 'String' },
0x23a2: { name: 'CodecPrivate', type: 'Binary' },
0x58688: { name: 'CodecName', type: 'String' },
0x3446: { name: 'AttachmentLink', type: 'Uint' },
0x1a9697: { name: 'CodecSettings', type: 'String' },
0x1b4040: { name: 'CodecInfoURL', type: 'String' },
0x6b240: { name: 'CodecDownloadURL', type: 'String' },
0x2a: { name: 'CodecDecodeAll', type: 'Uint' },
0x2fab: { name: 'TrackOverlay', type: 'Uint' },
0x16aa: { name: 'CodecDelay', type: 'Uint' },
0x16bb: { name: 'SeekPreRoll', type: 'Uint' },
0x2624: { name: 'TrackTranslate', type: 'Container' },
0x26fc: { name: 'TrackTranslateEditionUID', type: 'Uint' },
0x26bf: { name: 'TrackTranslateCodec', type: 'Uint' },
0x26a5: { name: 'TrackTranslateTrackID', type: 'Binary' },
0x60: { name: 'Video', type: 'Container' },
0x1a: { name: 'FlagInterlaced', type: 'Uint' },
0x13b8: { name: 'StereoMode', type: 'Uint' },
0x13c0: { name: 'AlphaMode', type: 'Uint' },
0x13b9: { name: 'OldStereoMode', type: 'Uint' },
0x30: { name: 'PixelWidth', type: 'Uint' },
0x3a: { name: 'PixelHeight', type: 'Uint' },
0x14aa: { name: 'PixelCropBottom', type: 'Uint' },
0x14bb: { name: 'PixelCropTop', type: 'Uint' },
0x14cc: { name: 'PixelCropLeft', type: 'Uint' },
0x14dd: { name: 'PixelCropRight', type: 'Uint' },
0x14b0: { name: 'DisplayWidth', type: 'Uint' },
0x14ba: { name: 'DisplayHeight', type: 'Uint' },
0x14b2: { name: 'DisplayUnit', type: 'Uint' },
0x14b3: { name: 'AspectRatioType', type: 'Uint' },
0xeb524: { name: 'ColourSpace', type: 'Binary' },
0xfb523: { name: 'GammaValue', type: 'Float' },
0x383e3: { name: 'FrameRate', type: 'Float' },
0x61: { name: 'Audio', type: 'Container' },
0x35: { name: 'SamplingFrequency', type: 'Float' },
0x38b5: { name: 'OutputSamplingFrequency', type: 'Float' },
0x1f: { name: 'Channels', type: 'Uint' },
0x3d7b: { name: 'ChannelPositions', type: 'Binary' },
0x2264: { name: 'BitDepth', type: 'Uint' },
0x62: { name: 'TrackOperation', type: 'Container' },
0x63: { name: 'TrackCombinePlanes', type: 'Container' },
0x64: { name: 'TrackPlane', type: 'Container' },
0x65: { name: 'TrackPlaneUID', type: 'Uint' },
0x66: { name: 'TrackPlaneType', type: 'Uint' },
0x69: { name: 'TrackJoinBlocks', type: 'Container' },
0x6d: { name: 'TrackJoinUID', type: 'Uint' },
0x40: { name: 'TrickTrackUID', type: 'Uint' },
0x41: { name: 'TrickTrackSegmentUID', type: 'Binary' },
0x46: { name: 'TrickTrackFlag', type: 'Uint' },
0x47: { name: 'TrickMasterTrackUID', type: 'Uint' },
0x44: { name: 'TrickMasterTrackSegmentUID', type: 'Binary' },
0x2d80: { name: 'ContentEncodings', type: 'Container' },
0x2240: { name: 'ContentEncoding', type: 'Container' },
0x1031: { name: 'ContentEncodingOrder', type: 'Uint' },
0x1032: { name: 'ContentEncodingScope', type: 'Uint' },
0x1033: { name: 'ContentEncodingType', type: 'Uint' },
0x1034: { name: 'ContentCompression', type: 'Container' },
0x254: { name: 'ContentCompAlgo', type: 'Uint' },
0x255: { name: 'ContentCompSettings', type: 'Binary' },
0x1035: { name: 'ContentEncryption', type: 'Container' },
0x7e1: { name: 'ContentEncAlgo', type: 'Uint' },
0x7e2: { name: 'ContentEncKeyID', type: 'Binary' },
0x7e3: { name: 'ContentSignature', type: 'Binary' },
0x7e4: { name: 'ContentSigKeyID', type: 'Binary' },
0x7e5: { name: 'ContentSigAlgo', type: 'Uint' },
0x7e6: { name: 'ContentSigHashAlgo', type: 'Uint' },
0xc53bb6b: { name: 'Cues', type: 'Container' },
0x3b: { name: 'CuePoint', type: 'Container' },
0x33: { name: 'CueTime', type: 'Uint' },
0x37: { name: 'CueTrackPositions', type: 'Container' },
0x77: { name: 'CueTrack', type: 'Uint' },
0x71: { name: 'CueClusterPosition', type: 'Uint' },
0x70: { name: 'CueRelativePosition', type: 'Uint' },
0x32: { name: 'CueDuration', type: 'Uint' },
0x1378: { name: 'CueBlockNumber', type: 'Uint' },
0x6a: { name: 'CueCodecState', type: 'Uint' },
0x5b: { name: 'CueReference', type: 'Container' },
0x16: { name: 'CueRefTime', type: 'Uint' },
0x17: { name: 'CueRefCluster', type: 'Uint' },
0x135f: { name: 'CueRefNumber', type: 'Uint' },
0x6b: { name: 'CueRefCodecState', type: 'Uint' },
0x941a469: { name: 'Attachments', type: 'Container' },
0x21a7: { name: 'AttachedFile', type: 'Container' },
0x67e: { name: 'FileDescription', type: 'String' },
0x66e: { name: 'FileName', type: 'String' },
0x660: { name: 'FileMimeType', type: 'String' },
0x65c: { name: 'FileData', type: 'Binary' },
0x6ae: { name: 'FileUID', type: 'Uint' },
0x675: { name: 'FileReferral', type: 'Binary' },
0x661: { name: 'FileUsedStartTime', type: 'Uint' },
0x662: { name: 'FileUsedEndTime', type: 'Uint' },
0x43a770: { name: 'Chapters', type: 'Container' },
0x5b9: { name: 'EditionEntry', type: 'Container' },
0x5bc: { name: 'EditionUID', type: 'Uint' },
0x5bd: { name: 'EditionFlagHidden', type: 'Uint' },
0x5db: { name: 'EditionFlagDefault', type: 'Uint' },
0x5dd: { name: 'EditionFlagOrdered', type: 'Uint' },
0x36: { name: 'ChapterAtom', type: 'Container' },
0x33c4: { name: 'ChapterUID', type: 'Uint' },
0x1654: { name: 'ChapterStringUID', type: 'String' },
0x11: { name: 'ChapterTimeStart', type: 'Uint' },
0x12: { name: 'ChapterTimeEnd', type: 'Uint' },
0x18: { name: 'ChapterFlagHidden', type: 'Uint' },
0x598: { name: 'ChapterFlagEnabled', type: 'Uint' },
0x2e67: { name: 'ChapterSegmentUID', type: 'Binary' },
0x2ebc: { name: 'ChapterSegmentEditionUID', type: 'Uint' },
0x23c3: { name: 'ChapterPhysicalEquiv', type: 'Uint' },
0xf: { name: 'ChapterTrack', type: 'Container' },
0x9: { name: 'ChapterTrackNumber', type: 'Uint' },
0x0: { name: 'ChapterDisplay', type: 'Container' },
0x5: { name: 'ChapString', type: 'String' },
0x37c: { name: 'ChapLanguage', type: 'String' },
0x37e: { name: 'ChapCountry', type: 'String' },
0x2944: { name: 'ChapProcess', type: 'Container' },
0x2955: { name: 'ChapProcessCodecID', type: 'Uint' },
0x50d: { name: 'ChapProcessPrivate', type: 'Binary' },
0x2911: { name: 'ChapProcessCommand', type: 'Container' },
0x2922: { name: 'ChapProcessTime', type: 'Uint' },
0x2933: { name: 'ChapProcessData', type: 'Binary' },
0x254c367: { name: 'Tags', type: 'Container' },
0x3373: { name: 'Tag', type: 'Container' },
0x23c0: { name: 'Targets', type: 'Container' },
0x28ca: { name: 'TargetTypeValue', type: 'Uint' },
0x23ca: { name: 'TargetType', type: 'String' },
0x23c5: { name: 'TagTrackUID', type: 'Uint' },
0x23c9: { name: 'TagEditionUID', type: 'Uint' },
0x23c4: { name: 'TagChapterUID', type: 'Uint' },
0x23c6: { name: 'TagAttachmentUID', type: 'Uint' },
0x27c8: { name: 'SimpleTag', type: 'Container' },
0x5a3: { name: 'TagName', type: 'String' },
0x47a: { name: 'TagLanguage', type: 'String' },
0x484: { name: 'TagDefault', type: 'Uint' },
0x487: { name: 'TagString', type: 'String' },
0x485: { name: 'TagBinary', type: 'Binary' }
};
function doInherit(newClass, baseClass) {
newClass.prototype = Object.create(baseClass.prototype);
newClass.prototype.constructor = newClass;
}
function WebmBase(name, type) {
this.name = name || 'Unknown';
this.type = type || 'Unknown';
}
WebmBase.prototype.updateBySource = function () { };
WebmBase.prototype.setSource = function (source) {
this.source = source;
this.updateBySource();
};
WebmBase.prototype.updateByData = function () { };
WebmBase.prototype.setData = function (data) {
this.data = data;
this.updateByData();
};
function WebmUint(name, type) {
WebmBase.call(this, name, type || 'Uint');
}
doInherit(WebmUint, WebmBase);
function padHex(hex) {
return hex.length % 2 === 1 ? '0' + hex : hex;
}
WebmUint.prototype.updateBySource = function () {
// use hex representation of a number instead of number value
this.data = '';
for (var i = 0; i < this.source.length; i++) {
var hex = this.source[i].toString(16);
this.data += padHex(hex);
}
};
WebmUint.prototype.updateByData = function () {
var length = this.data.length / 2;
this.source = new Uint8Array(length);
for (var i = 0; i < length; i++) {
var hex = this.data.substr(i * 2, 2);
this.source[i] = parseInt(hex, 16);
}
};
WebmUint.prototype.getValue = function () {
return parseInt(this.data, 16);
};
WebmUint.prototype.setValue = function (value) {
this.setData(padHex(value.toString(16)));
};
function WebmFloat(name, type) {
WebmBase.call(this, name, type || 'Float');
}
doInherit(WebmFloat, WebmBase);
WebmFloat.prototype.getFloatArrayType = function () {
return this.source && this.source.length === 4 ? Float32Array : Float64Array;
};
WebmFloat.prototype.updateBySource = function () {
var byteArray = this.source.reverse();
var floatArrayType = this.getFloatArrayType();
var floatArray = new floatArrayType(byteArray.buffer);
this.data = floatArray[0];
};
WebmFloat.prototype.updateByData = function () {
var floatArrayType = this.getFloatArrayType();
var floatArray = new floatArrayType([this.data]);
var byteArray = new Uint8Array(floatArray.buffer);
this.source = byteArray.reverse();
};
WebmFloat.prototype.getValue = function () {
return this.data;
};
WebmFloat.prototype.setValue = function (value) {
this.setData(value);
};
function WebmContainer(name, type) {
WebmBase.call(this, name, type || 'Container');
}
doInherit(WebmContainer, WebmBase);
WebmContainer.prototype.readByte = function () {
return this.source[this.offset++];
};
WebmContainer.prototype.readUint = function () {
var firstByte = this.readByte();
var bytes = 8 - firstByte.toString(2).length;
var value = firstByte - (1 << (7 - bytes));
for (var i = 0; i < bytes; i++) {
// don't use bit operators to support x86
value *= 256;
value += this.readByte();
}
return value;
};
WebmContainer.prototype.updateBySource = function () {
this.data = [];
for (this.offset = 0; this.offset < this.source.length; this.offset = end) {
var id = this.readUint();
var len = this.readUint();
var end = Math.min(this.offset + len, this.source.length);
var data = this.source.slice(this.offset, end);
var info = sections[id] || { name: 'Unknown', type: 'Unknown' };
var ctr = WebmBase;
switch (info.type) {
case 'Container':
ctr = WebmContainer;
break;
case 'Uint':
ctr = WebmUint;
break;
case 'Float':
ctr = WebmFloat;
break;
}
var section = new ctr(info.name, info.type);
section.setSource(data);
this.data.push({
id: id,
idHex: id.toString(16),
data: section
});
}
};
WebmContainer.prototype.writeUint = function (x, draft) {
for (var bytes = 1, flag = 0x80; x >= flag && bytes < 8; bytes++, flag *= 0x80) { }
if (!draft) {
var value = flag + x;
for (var i = bytes - 1; i >= 0; i--) {
// don't use bit operators to support x86
var c = value % 256;
this.source[this.offset + i] = c;
value = (value - c) / 256;
}
}
this.offset += bytes;
};
WebmContainer.prototype.writeSections = function (draft) {
this.offset = 0;
for (var i = 0; i < this.data.length; i++) {
var section = this.data[i],
content = section.data.source,
contentLength = content.length;
this.writeUint(section.id, draft);
this.writeUint(contentLength, draft);
if (!draft) {
this.source.set(content, this.offset);
}
this.offset += contentLength;
}
return this.offset;
};
WebmContainer.prototype.updateByData = function () {
// run without accessing this.source to determine total length - need to know it to create Uint8Array
var length = this.writeSections('draft');
this.source = new Uint8Array(length);
// now really write data
this.writeSections();
};
WebmContainer.prototype.getSectionById = function (id) {
for (var i = 0; i < this.data.length; i++) {
var section = this.data[i];
if (section.id === id) {
return section.data;
}
}
return null;
};
function WebmFile(source) {
WebmContainer.call(this, 'File', 'File');
this.setSource(source);
}
doInherit(WebmFile, WebmContainer);
WebmFile.prototype.fixDuration = function (duration, options) {
var logger = options && options.logger;
if (logger === undefined) {
logger = function (message) {
console.log(message);
};
} else if (!logger) {
logger = function () { };
}
var segmentSection = this.getSectionById(0x8538067);
if (!segmentSection) {
logger('[fix-webm-duration] Segment section is missing');
return false;
}
var infoSection = segmentSection.getSectionById(0x549a966);
if (!infoSection) {
logger('[fix-webm-duration] Info section is missing');
return false;
}
var timeScaleSection = infoSection.getSectionById(0xad7b1);
if (!timeScaleSection) {
logger('[fix-webm-duration] TimecodeScale section is missing');
return false;
}
var durationSection = infoSection.getSectionById(0x489);
if (durationSection) {
if (durationSection.getValue() <= 0) {
logger('[fix-webm-duration] Duration section is present, but the value is empty. Applying ' + duration.toLocaleString() + ' ms.');
durationSection.setValue(duration);
} else {
logger('[fix-webm-duration] Duration section is present');
return false;
}
} else {
logger('[fix-webm-duration] Duration section is missing. Applying ' + duration.toLocaleString() + ' ms.');
// append Duration section
durationSection = new WebmFloat('Duration', 'Float');
durationSection.setValue(duration);
infoSection.data.push({
id: 0x489,
data: durationSection
});
}
// set default time scale to 1 millisecond (1000000 nanoseconds)
timeScaleSection.setValue(1000000);
infoSection.updateByData();
segmentSection.updateByData();
this.updateByData();
return true;
};
WebmFile.prototype.toBlob = function (mimeType) {
return new Blob([this.source.buffer], { type: mimeType || 'audio/webm' });
};
function fixWebmDuration(blob, duration, callback, options) {
// The callback may be omitted - then the third argument is options
if (typeof callback === "object") {
options = callback;
callback = undefined;
}
if (!callback) {
return new Promise(function (resolve) {
fixWebmDuration(blob, duration, resolve, options);
});
}
try {
var reader = new FileReader();
reader.onloadend = function () {
try {
var file = new WebmFile(new Uint8Array(reader.result));
if (file.fixDuration(duration, options)) {
blob = file.toBlob(blob.type);
}
} catch (ex) {
// ignore
}
callback(blob);
};
reader.readAsArrayBuffer(blob);
} catch (ex) {
callback(blob);
}
}
// Support AMD import default
fixWebmDuration.default = fixWebmDuration;
return fixWebmDuration;
});

View File

@ -1,407 +0,0 @@
class RecorderProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: 'numChannels',
defaultValue: 1,
minValue: 1,
maxValue: 16
},
{
name: 'sampleRate',
defaultValue: 48000,
minValue: 8000,
maxValue: 96000
},
{
name: 'streamBufferSize',
defaultValue: 2048,
minValue: 256,
maxValue: 8192
}
];
}
// Buffer size compromise between size and process call frequency
_bufferSize = 2048
// The current buffer fill level
_bytesWritten = 0
// Buffer per channel
_buffers = []
// Resampler (passthrough, down or up)
_resampler = null
// Config
_numChannels = 1
_sampleRate = 48000
constructor(options) {
super(options)
this._numChannels = options.parameterData.numChannels
this._sampleRate = options.parameterData.sampleRate
this._bufferSize = options.parameterData.streamBufferSize
// Resampler(current context sample rate, desired sample rate, num channels, buffer size)
// num channels is always 1 since we resample after interleaving channels
this._resampler = new Resampler(sampleRate, this._sampleRate, 1, this._bufferSize * this._numChannels)
this.initBuffers()
}
initBuffers() {
this._bytesWritten = 0
this._buffers = []
for (let channel = 0; channel < this._numChannels; channel++) {
this._buffers[channel] = []
}
}
/**
* @returns {boolean}
*/
isBufferEmpty() {
return this._bytesWritten === 0
}
/**
* @returns {boolean}
*/
isBufferFull() {
return this._bytesWritten >= this._bufferSize
}
/**
* @param {Float32Array[][]} inputs
* @returns {boolean}
*/
process(inputs) {
if (this.isBufferFull()) {
this.flush()
}
const input = inputs[0]
if (input.length == 0) {
// Sometimes, Firefox doesn't give any input. Skip this frame to not fail.
return true
}
for (let channel = 0; channel < this._numChannels; channel++) {
// Push a copy of the array.
// The underlying implementation may reuse it which will break the recording.
this._buffers[channel].push([...input[channel % input.length]])
}
this._bytesWritten += input[0].length
return true
}
flush() {
let channels = []
for (let channel = 0; channel < this._numChannels; channel++) {
channels.push(this.mergeFloat32Arrays(this._buffers[channel], this._bytesWritten))
}
let interleaved = this.interleave(channels)
let resampled = this._resampler.resample(interleaved)
this.port.postMessage(this.floatTo16BitPCM(resampled))
this.initBuffers()
}
mergeFloat32Arrays(arrays, bytesWritten) {
let result = new Float32Array(bytesWritten)
var offset = 0
for (let i = 0; i < arrays.length; i++) {
result.set(arrays[i], offset)
offset += arrays[i].length
}
return result
}
// Interleave data from channels from LLLLRRRR to LRLRLRLR
interleave(channels) {
if (channels === 1) {
return channels[0]
}
var length = 0
for (let i = 0; i < channels.length; i++) {
length += channels[i].length
}
let result = new Float32Array(length)
var index = 0
var inputIndex = 0
while (index < length) {
for (let i = 0; i < channels.length; i++) {
result[index] = channels[i][inputIndex]
index++
}
inputIndex++
}
return result
}
floatTo16BitPCM(input) {
let output = new DataView(new ArrayBuffer(input.length * 2))
for (let i = 0; i < input.length; i++) {
let s = Math.max(-1, Math.min(1, input[i]))
let s16 = s < 0 ? s * 0x8000 : s * 0x7FFF
output.setInt16(i * 2, s16, true)
}
return new Int16Array(output.buffer)
}
}
class Resampler {
constructor(fromSampleRate, toSampleRate, channels, inputBufferSize) {
if (!fromSampleRate || !toSampleRate || !channels) {
throw (new Error("Invalid settings specified for the resampler."));
}
this.resampler = null;
this.fromSampleRate = fromSampleRate;
this.toSampleRate = toSampleRate;
this.channels = channels || 0;
this.inputBufferSize = inputBufferSize;
this.initialize()
}
initialize() {
if (this.fromSampleRate == this.toSampleRate) {
// Setup resampler bypass - Resampler just returns what was passed through
this.resampler = (buffer) => {
return buffer
};
this.ratioWeight = 1;
} else {
if (this.fromSampleRate < this.toSampleRate) {
// Use generic linear interpolation if upsampling,
// as linear interpolation produces a gradient that we want
// and works fine with two input sample points per output in this case.
this.linearInterpolation();
this.lastWeight = 1;
} else {
// Custom resampler I wrote that doesn't skip samples
// like standard linear interpolation in high downsampling.
// This is more accurate than linear interpolation on downsampling.
this.multiTap();
this.tailExists = false;
this.lastWeight = 0;
}
// Initialize the internal buffer:
this.initializeBuffers();
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
}
}
bufferSlice(sliceAmount) {
//Typed array and normal array buffer section referencing:
try {
return this.outputBuffer.subarray(0, sliceAmount);
}
catch (error) {
try {
//Regular array pass:
this.outputBuffer.length = sliceAmount;
return this.outputBuffer;
}
catch (error) {
//Nightly Firefox 4 used to have the subarray function named as slice:
return this.outputBuffer.slice(0, sliceAmount);
}
}
}
initializeBuffers() {
this.outputBufferSize = (Math.ceil(this.inputBufferSize * this.toSampleRate / this.fromSampleRate / this.channels * 1.000000476837158203125) + this.channels) + this.channels;
try {
this.outputBuffer = new Float32Array(this.outputBufferSize);
this.lastOutput = new Float32Array(this.channels);
}
catch (error) {
this.outputBuffer = [];
this.lastOutput = [];
}
}
linearInterpolation() {
this.resampler = (buffer) => {
let bufferLength = buffer.length,
channels = this.channels,
outLength,
ratioWeight,
weight,
firstWeight,
secondWeight,
sourceOffset,
outputOffset,
outputBuffer,
channel;
if ((bufferLength % channels) !== 0) {
throw (new Error("Buffer was of incorrect sample length."));
}
if (bufferLength <= 0) {
return [];
}
outLength = this.outputBufferSize;
ratioWeight = this.ratioWeight;
weight = this.lastWeight;
firstWeight = 0;
secondWeight = 0;
sourceOffset = 0;
outputOffset = 0;
outputBuffer = this.outputBuffer;
for (; weight < 1; weight += ratioWeight) {
secondWeight = weight % 1;
firstWeight = 1 - secondWeight;
this.lastWeight = weight % 1;
for (channel = 0; channel < this.channels; ++channel) {
outputBuffer[outputOffset++] = (this.lastOutput[channel] * firstWeight) + (buffer[channel] * secondWeight);
}
}
weight -= 1;
for (bufferLength -= channels, sourceOffset = Math.floor(weight) * channels; outputOffset < outLength && sourceOffset < bufferLength;) {
secondWeight = weight % 1;
firstWeight = 1 - secondWeight;
for (channel = 0; channel < this.channels; ++channel) {
outputBuffer[outputOffset++] = (buffer[sourceOffset + ((channel > 0) ? (channel) : 0)] * firstWeight) + (buffer[sourceOffset + (channels + channel)] * secondWeight);
}
weight += ratioWeight;
sourceOffset = Math.floor(weight) * channels;
}
for (channel = 0; channel < channels; ++channel) {
this.lastOutput[channel] = buffer[sourceOffset++];
}
return this.bufferSlice(outputOffset);
};
}
multiTap() {
this.resampler = (buffer) => {
let bufferLength = buffer.length,
outLength,
output_variable_list,
channels = this.channels,
ratioWeight,
weight,
channel,
actualPosition,
amountToNext,
alreadyProcessedTail,
outputBuffer,
outputOffset,
currentPosition;
if ((bufferLength % channels) !== 0) {
throw (new Error("Buffer was of incorrect sample length."));
}
if (bufferLength <= 0) {
return [];
}
outLength = this.outputBufferSize;
output_variable_list = [];
ratioWeight = this.ratioWeight;
weight = 0;
actualPosition = 0;
amountToNext = 0;
alreadyProcessedTail = !this.tailExists;
this.tailExists = false;
outputBuffer = this.outputBuffer;
outputOffset = 0;
currentPosition = 0;
for (channel = 0; channel < channels; ++channel) {
output_variable_list[channel] = 0;
}
do {
if (alreadyProcessedTail) {
weight = ratioWeight;
for (channel = 0; channel < channels; ++channel) {
output_variable_list[channel] = 0;
}
} else {
weight = this.lastWeight;
for (channel = 0; channel < channels; ++channel) {
output_variable_list[channel] = this.lastOutput[channel];
}
alreadyProcessedTail = true;
}
while (weight > 0 && actualPosition < bufferLength) {
amountToNext = 1 + actualPosition - currentPosition;
if (weight >= amountToNext) {
for (channel = 0; channel < channels; ++channel) {
output_variable_list[channel] += buffer[actualPosition++] * amountToNext;
}
currentPosition = actualPosition;
weight -= amountToNext;
} else {
for (channel = 0; channel < channels; ++channel) {
output_variable_list[channel] += buffer[actualPosition + ((channel > 0) ? channel : 0)] * weight;
}
currentPosition += weight;
weight = 0;
break;
}
}
if (weight === 0) {
for (channel = 0; channel < channels; ++channel) {
outputBuffer[outputOffset++] = output_variable_list[channel] / ratioWeight;
}
} else {
this.lastWeight = weight;
for (channel = 0; channel < channels; ++channel) {
this.lastOutput[channel] = output_variable_list[channel];
}
this.tailExists = true;
break;
}
} while (actualPosition < bufferLength && outputOffset < outLength);
return this.bufferSlice(outputOffset);
};
}
resample(buffer) {
if (this.fromSampleRate == this.toSampleRate) {
this.ratioWeight = 1;
} else {
if (this.fromSampleRate < this.toSampleRate) {
this.lastWeight = 1;
} else {
this.tailExists = false;
this.lastWeight = 0;
}
this.initializeBuffers();
this.ratioWeight = this.fromSampleRate / this.toSampleRate;
}
return this.resampler(buffer)
}
}
registerProcessor("recorder.worklet", RecorderProcessor)

View File

@ -1,44 +0,0 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.yuanxuan.yx_asr_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yuanxuan.yx_asr_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@ -1,21 +0,0 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,25 +0,0 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

View File

@ -15,7 +15,7 @@ class MyApp extends StatelessWidget {
title: 'YX ASR Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
useMaterial3: false, // Material 3
),
home: const SpeechRecognitionPage(),
);
@ -40,10 +40,9 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
bool _isListening = false;
String _currentText = '';
String _errorMessage = '';
List<String> _recognitionHistory = [];
//
final List<String> _realtimeResults = []; //
///
String _baseText = '';
@override
void initState() {
@ -95,8 +94,6 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
if (result.recognizedWords.isNotEmpty) {
print('📱 [Example] 实时识别: ${result.recognizedWords}');
_currentText = result.recognizedWords;
//
_updateTextController();
}
});
});
@ -151,83 +148,16 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
);
}
///
void _clearHistory() {
///
void _clearContent() {
setState(() {
_recognitionHistory.clear();
_realtimeResults.clear();
_currentText = '';
_baseText = ''; //
_textController.clear();
_errorMessage = '';
});
}
///
void _updateTextController() {
//
final confirmedText = _realtimeResults.join(' ');
final displayText = _currentText.isNotEmpty
? '$confirmedText ${_currentText}'.trim()
: confirmedText;
//
if (_textController.text != displayText) {
final cursorPosition = _textController.selection.baseOffset;
_textController.text = displayText;
//
if (cursorPosition <= displayText.length) {
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: cursorPosition),
);
} else {
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: displayText.length),
);
}
}
}
///
Future<void> _toggleRecording() async {
if (!_isInitialized) return;
try {
if (_isListening) {
print('📱 [Example] 停止录音');
await _speechService.stopListening();
//
if (_currentText.isNotEmpty) {
setState(() {
_realtimeResults.add(_currentText);
_recognitionHistory.insert(0, _currentText);
print('📱 [Example] 添加到历史记录: $_currentText');
//
if (_recognitionHistory.length > 10) {
_recognitionHistory.removeLast();
}
//
_currentText = '';
_updateTextController();
});
}
} else {
print('📱 [Example] 开始录音');
//
setState(() {
_realtimeResults.clear();
_currentText = '';
_textController.clear();
});
await _speechService.startListening(partialResults: true);
}
} catch (e) {
print('📱 [Example] 录音操作失败: $e');
_showErrorSnackBar('录音操作失败: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -237,8 +167,8 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
actions: [
IconButton(
icon: const Icon(Icons.clear_all),
onPressed: _clearHistory,
tooltip: '清除历史',
onPressed: _clearContent,
tooltip: '清除内容',
),
],
),
@ -253,9 +183,6 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
//
_buildRecognitionCard(),
const SizedBox(height: 16),
//
Expanded(child: _buildHistoryCard()),
],
),
),
@ -427,13 +354,17 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
children: [
Icon(Icons.mic, size: 14, color: Colors.blue[600]),
const SizedBox(width: 4),
Text(
'实时识别中...',
Flexible(
child: Text(
_currentText.isEmpty ? '实时识别中...' : _currentText,
style: TextStyle(
fontSize: 12,
color: Colors.blue[600],
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
@ -456,66 +387,6 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
);
}
///
Widget _buildHistoryCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'识别历史',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Expanded(
child: _recognitionHistory.isEmpty
? Center(
child: Text(
'暂无识别历史',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
)
: ListView.builder(
itemCount: _recognitionHistory.length,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.only(bottom: 8.0),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
child: Text(
'${index + 1}',
style: const TextStyle(color: Colors.white),
),
),
title: Text(_recognitionHistory[index]),
trailing: IconButton(
icon: const Icon(Icons.copy),
onPressed: () {
// TODO:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已复制到剪贴板'),
),
);
},
),
),
);
},
),
),
],
),
),
);
}
///
Widget _buildFloatingActionButton() {
if (!_isInitialized) {
@ -526,44 +397,63 @@ class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
);
}
return GestureDetector(
onTap: () {
//
HapticFeedback.mediumImpact();
_toggleRecording();
// 使 RecordingButton
return RecordingButton(
speechService: _speechService,
size: 80,
onResult: (result) {
print(
'📱 [Example] RecordingButton 接收到识别结果: "${result.recognizedWords}"');
setState(() {
if (result.recognizedWords.isNotEmpty) {
//
print('📱 [Example] 实时识别,更新输入框: ${result.recognizedWords}');
_currentText = result.recognizedWords;
// = +
String newText = _baseText;
if (newText.isNotEmpty &&
!newText.endsWith(' ') &&
_currentText.isNotEmpty) {
newText += ' '; //
}
newText += _currentText;
_textController.text = newText;
//
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: newText.length),
);
}
});
},
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isListening
? [Colors.red.shade400, Colors.red.shade600]
: [Colors.blue.shade400, Colors.blue.shade600],
),
boxShadow: [
BoxShadow(
color: (_isListening ? Colors.red : Colors.blue)
.withValues(alpha: 0.4),
spreadRadius: 4,
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Icon(
_isListening ? Icons.stop_rounded : Icons.mic_rounded,
key: ValueKey(_isListening),
size: 32,
color: Colors.white,
),
),
),
onError: (error) {
setState(() {
_errorMessage = error.errorMsg;
});
_showErrorSnackBar('识别错误: ${error.errorMsg}');
},
onListeningStatusChanged: (isListening) {
setState(() {
_isListening = isListening;
});
if (!isListening) {
//
setState(() {
_baseText = _textController.text; //
_currentText = ''; //
});
} else {
//
setState(() {
_baseText = _textController.text; //
_currentText = ''; //
});
}
},
tooltip: _isListening ? '点击停止录音' : '点击开始录音',
enabled: _isInitialized,
);
}
}

View File

@ -24,3 +24,13 @@ flutter:
# 配置模型文件 assets
assets:
- assets/models/
- assets/models/sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23/
- assets/models/sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23/encoder-epoch-99-avg-1.int8.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23/decoder-epoch-99-avg-1.int8.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23/joiner-epoch-99-avg-1.int8.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-14M-2023-02-23/tokens.txt
- assets/models/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30/
- assets/models/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30/encoder.int8.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30/decoder.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30/joiner.int8.onnx
- assets/models/sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30/tokens.txt

View File

@ -1,14 +0,0 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/Users/max/fvm/versions/3.32.0
FLUTTER_APPLICATION_PATH=/Users/max/SourceCode/yuanxuan/yx_asr
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1.0.0
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json

View File

@ -1,32 +0,0 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View File

@ -1,5 +0,0 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@ -1,13 +0,0 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/Users/max/fvm/versions/3.32.0"
export "FLUTTER_APPLICATION_PATH=/Users/max/SourceCode/yuanxuan/yx_asr"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1.0.0"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

View File

@ -3,24 +3,14 @@ class SpeechRecognitionResult {
///
final String recognizedWords;
/// 0.0 1.0
final double confidence;
///
final List<String> alternatives;
const SpeechRecognitionResult({
required this.recognizedWords,
this.confidence = 0.0,
this.alternatives = const [],
});
/// Map [SpeechRecognitionResult]
factory SpeechRecognitionResult.fromMap(Map<String, dynamic> map) {
return SpeechRecognitionResult(
recognizedWords: map['recognizedWords'] as String? ?? '',
confidence: (map['confidence'] as num?)?.toDouble() ?? 0.0,
alternatives: List<String>.from(map['alternatives'] as List? ?? []),
);
}
@ -28,31 +18,23 @@ class SpeechRecognitionResult {
Map<String, dynamic> toMap() {
return {
'recognizedWords': recognizedWords,
'confidence': confidence,
'alternatives': alternatives,
};
}
@override
String toString() {
return 'SpeechRecognitionResult(recognizedWords: $recognizedWords, '
'confidence: $confidence, alternatives: $alternatives)';
return 'SpeechRecognitionResult(recognizedWords: $recognizedWords)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is SpeechRecognitionResult &&
other.recognizedWords == recognizedWords &&
other.confidence == confidence &&
other.alternatives.length == alternatives.length &&
other.alternatives.every((alt) => alternatives.contains(alt));
other.recognizedWords == recognizedWords;
}
@override
int get hashCode {
return recognizedWords.hashCode ^
confidence.hashCode ^
alternatives.hashCode;
return recognizedWords.hashCode;
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../interfaces/speech_recognition_service.dart';
import '../yx_asr_service.dart';
import '../models/speech_recognition_result.dart';
@ -69,6 +70,7 @@ class _RecordingButtonState extends State<RecordingButton>
late SpeechRecognitionService _speechService;
bool _isListening = false;
bool _isInitialized = false;
bool _isProcessing = false; //
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@ -146,13 +148,29 @@ class _RecordingButtonState extends State<RecordingButton>
}
Future<void> _toggleRecording() async {
if (!_isInitialized || !widget.enabled) return;
//
if (_isProcessing || !_isInitialized || !widget.enabled) return;
//
setState(() {
_isProcessing = true;
});
try {
//
await HapticFeedback.lightImpact();
//
_animationController.forward().then((_) {
_animationController.reverse();
});
if (_isListening) {
await _speechService.stopListening();
} else {
await _speechService.startListening(partialResults: widget.partialResults);
await _speechService.startListening(
partialResults: widget.partialResults,
);
}
} catch (e) {
widget.onError?.call(SpeechRecognitionError(
@ -160,52 +178,75 @@ class _RecordingButtonState extends State<RecordingButton>
errorMsg: '切换录音状态失败: $e',
errorCode: null,
));
} finally {
//
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Color buttonColor;
Color iconColor;
if (!widget.enabled || !_isInitialized) {
buttonColor = widget.disabledColor ?? Colors.grey;
} else if (_isListening) {
buttonColor = widget.recordingColor ?? Colors.red;
iconColor = widget.disabledColor ?? Colors.grey[850]!;
} else {
buttonColor = widget.idleColor ?? theme.primaryColor;
iconColor = _isListening
? (widget.recordingColor ?? const Color(0xFFFF5252))
: (widget.idleColor ?? const Color(0xFF2196F3));
}
Widget button = AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
return Container(
width: widget.size,
height: widget.size,
decoration: BoxDecoration(
color: buttonColor,
color: iconColor.withValues(alpha: 0.12),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: buttonColor.withValues(alpha: 0.3),
blurRadius: _isListening ? 20 : 8,
spreadRadius: _isListening ? 5 : 2,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(widget.size / 2),
onTap: _toggleRecording,
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 0,
top: 0,
child: Icon(
_isListening ? Icons.stop : Icons.mic,
size: widget.size * 0.4,
color: Colors.white,
_isListening ? Icons.stop_rounded : Icons.mic_rounded,
size: widget.size * 0.55,
color: iconColor,
),
),
if (_isProcessing || _isListening)
Positioned(
left: 0,
right: 0,
bottom: 0,
top: 0,
child: SizedBox(
width: widget.size,
height: widget.size,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
),
)
],
),
),
),
);

View File

@ -221,6 +221,7 @@ class YxAsrService implements SpeechRecognitionService {
bool _isStartingRecording = false; //
bool _isInitialized = false;
String _currentModelPath = '';
String _lastRecognizedText = ''; //
//
RecognitionSpeed _recognitionSpeed = RecognitionSpeed.fast;
@ -513,6 +514,7 @@ class YxAsrService implements SpeechRecognitionService {
await _startAudioRecording(_sampleRate.hz);
_isListening = true;
_lastRecognizedText = ''; //
_statusController.add(true);
//
@ -572,29 +574,8 @@ class YxAsrService implements SpeechRecognitionService {
//
await _stopAudioRecording();
//
if (_recognizer != null && _stream != null) {
try {
// 线使
debugPrint('🛑 [YxAsr] 获取流式识别最终结果...');
final result = _recognizer!.getResult(_stream!);
debugPrint('🛑 [YxAsr] 流式最终结果: "${result.text}"');
if (result.text.isNotEmpty) {
debugPrint('📤 [YxAsr] 发送流式最终结果: ${result.text}');
_sendResult(
recognizedWords: result.text,
confidence: 1.0,
alternatives: [],
);
} else {
debugPrint('⚠️ [YxAsr] 流式最终结果为空');
}
} catch (e) {
debugPrint('⚠️ [YxAsr] 获取最终结果时出错: $e');
}
//
if (_stream != null) {
_stream = null;
}
@ -761,13 +742,18 @@ class YxAsrService implements SpeechRecognitionService {
final result = _recognizer!.getResult(_stream!);
debugPrint('🔍 [YxAsr] 获取识别结果: "${result.text}"');
if (result.text.isNotEmpty && partialResults) {
//
if (result.text.isNotEmpty &&
partialResults &&
result.text != _lastRecognizedText) {
debugPrint('🎤 [YxAsr] 发送实时识别结果: ${result.text}');
_lastRecognizedText = result.text; //
_sendResult(
recognizedWords: result.text,
confidence: 0.8,
alternatives: [],
);
} else if (result.text.isNotEmpty &&
result.text == _lastRecognizedText) {
debugPrint('🔄 [YxAsr] 跳过重复识别结果: "${result.text}"');
}
//
@ -783,14 +769,10 @@ class YxAsrService implements SpeechRecognitionService {
///
void _sendResult({
required String recognizedWords,
required double confidence,
required List<String> alternatives,
}) {
debugPrint('📤 [YxAsr] 发送识别结果: "$recognizedWords"');
final result = SpeechRecognitionResult(
recognizedWords: recognizedWords,
confidence: confidence,
alternatives: alternatives,
);
_resultController.add(result);
}

View File

@ -24,5 +24,10 @@ dev_dependencies:
flutter_lints: ^3.0.0
flutter:
assets:
- assets/models/
plugin:
platforms:
android:
package: com.yuanxuan.yx_asr
pluginClass: YxAsrPlugin
ios:
pluginClass: YxAsrPlugin