react-native-kookit 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG_CONTENT_URI.md +0 -130
- package/IMPLEMENTATION_SUMMARY.md +0 -206
- package/README.md +1 -0
- package/TTS_CHECKLIST.md +256 -0
- package/android/src/main/java/expo/modules/kookit/ReactNativeKookitModule.kt +145 -0
- package/build/ReactNativeKookit.types.d.ts +11 -0
- package/build/ReactNativeKookit.types.d.ts.map +1 -1
- package/build/ReactNativeKookit.types.js.map +1 -1
- package/build/ReactNativeKookitModule.d.ts +9 -1
- package/build/ReactNativeKookitModule.d.ts.map +1 -1
- package/build/ReactNativeKookitModule.js.map +1 -1
- package/ios/ReactNativeKookitModule.swift +143 -0
- package/package.json +1 -1
package/CHANGELOG_CONTENT_URI.md
CHANGED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# Changelog - Content URI Handler
|
|
2
|
-
|
|
3
|
-
## [Unreleased] - 2025-10-09
|
|
4
|
-
|
|
5
|
-
### Added
|
|
6
|
-
|
|
7
|
-
#### Content URI Handler (Android)
|
|
8
|
-
通过 Android ContentResolver 处理 `content://` URI,解决权限限制问题。
|
|
9
|
-
|
|
10
|
-
**新增 API:**
|
|
11
|
-
- `copyContentUriToCache(contentUri, fileName?)` - 复制 content URI 到缓存目录
|
|
12
|
-
- `getContentUriMetadata(contentUri)` - 获取文件元数据(名称、大小、MIME 类型)
|
|
13
|
-
|
|
14
|
-
**核心功能:**
|
|
15
|
-
- ✅ 使用 ContentResolver 绕过权限限制
|
|
16
|
-
- ✅ 无需 `READ_EXTERNAL_STORAGE` 权限
|
|
17
|
-
- ✅ 支持所有 ContentProvider(文件管理器、下载管理器、第三方应用)
|
|
18
|
-
- ✅ 8KB 缓冲区流式复制
|
|
19
|
-
- ✅ 后台线程执行,避免阻塞 UI
|
|
20
|
-
- ✅ 完整的错误处理和资源清理
|
|
21
|
-
- ✅ TypeScript 类型定义
|
|
22
|
-
|
|
23
|
-
**解决的问题:**
|
|
24
|
-
- `SecurityException: Permission Denial` when accessing content:// URIs
|
|
25
|
-
- 第三方应用未正确授予 URI 读取权限
|
|
26
|
-
- expo-file-system 无法访问某些 content:// URIs
|
|
27
|
-
|
|
28
|
-
**文档:**
|
|
29
|
-
- 📖 [完整文档](./docs/CONTENT_URI_HANDLER.md)
|
|
30
|
-
- 💡 [快速示例](./example/ContentUriQuickExamples.ts)
|
|
31
|
-
- 🎨 [UI 示例组件](./example/ContentUriExample.tsx)
|
|
32
|
-
- 📝 [实现总结](./IMPLEMENTATION_SUMMARY.md)
|
|
33
|
-
|
|
34
|
-
**使用场景:**
|
|
35
|
-
- 处理"打开方式" Intent
|
|
36
|
-
- 处理"分享到" Intent
|
|
37
|
-
- 从文档选择器获取文件
|
|
38
|
-
- 从第三方文件管理器接收文件
|
|
39
|
-
- 与 FTP/SMB 客户端集成上传
|
|
40
|
-
|
|
41
|
-
**技术细节:**
|
|
42
|
-
- Platform: Android (API 19+)
|
|
43
|
-
- 实现语言: Kotlin
|
|
44
|
-
- 异步执行: Coroutines (Dispatchers.IO)
|
|
45
|
-
- 缓存位置: Context.getCacheDir()
|
|
46
|
-
|
|
47
|
-
### Changed
|
|
48
|
-
- 更新 README.md,添加功能列表和 Content URI Handler 介绍
|
|
49
|
-
|
|
50
|
-
### Technical Notes
|
|
51
|
-
|
|
52
|
-
**原生模块更改:**
|
|
53
|
-
```kotlin
|
|
54
|
-
// 新增导入
|
|
55
|
-
import android.net.Uri
|
|
56
|
-
import android.provider.OpenableColumns
|
|
57
|
-
import android.database.Cursor
|
|
58
|
-
import java.io.File
|
|
59
|
-
import java.io.FileOutputStream
|
|
60
|
-
import java.io.InputStream
|
|
61
|
-
|
|
62
|
-
// 新增方法
|
|
63
|
-
AsyncFunction("copyContentUriToCache")
|
|
64
|
-
AsyncFunction("getContentUriMetadata")
|
|
65
|
-
|
|
66
|
-
// 新增辅助方法
|
|
67
|
-
private fun getContentUriMetadataInternal(context: Context, uri: Uri)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**TypeScript 接口更改:**
|
|
71
|
-
```typescript
|
|
72
|
-
// src/ReactNativeKookitModule.ts
|
|
73
|
-
copyContentUriToCache(contentUri: string, fileName?: string): Promise<{...}>
|
|
74
|
-
getContentUriMetadata(contentUri: string): Promise<{...}>
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Migration Guide
|
|
78
|
-
|
|
79
|
-
此为新增功能,无需迁移。现有代码不受影响。
|
|
80
|
-
|
|
81
|
-
### Example Usage
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
import ReactNativeKookitModule from 'react-native-kookit';
|
|
85
|
-
|
|
86
|
-
// 复制文件
|
|
87
|
-
const result = await ReactNativeKookitModule.copyContentUriToCache(
|
|
88
|
-
'content://...',
|
|
89
|
-
'filename.pdf'
|
|
90
|
-
);
|
|
91
|
-
console.log('Local path:', result.localPath);
|
|
92
|
-
|
|
93
|
-
// 获取元数据
|
|
94
|
-
const metadata = await ReactNativeKookitModule.getContentUriMetadata(
|
|
95
|
-
'content://...'
|
|
96
|
-
);
|
|
97
|
-
console.log('File info:', metadata);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Breaking Changes
|
|
101
|
-
|
|
102
|
-
无
|
|
103
|
-
|
|
104
|
-
### Deprecations
|
|
105
|
-
|
|
106
|
-
无
|
|
107
|
-
|
|
108
|
-
### Security
|
|
109
|
-
|
|
110
|
-
- 此功能仅访问应用明确接收的 content:// URIs
|
|
111
|
-
- 文件复制到应用私有缓存目录,其他应用无法访问
|
|
112
|
-
- 无需请求存储权限
|
|
113
|
-
- 符合 Android 安全最佳实践
|
|
114
|
-
|
|
115
|
-
### Performance
|
|
116
|
-
|
|
117
|
-
- 使用 8KB 缓冲区优化复制速度
|
|
118
|
-
- 在后台线程执行,不阻塞 UI
|
|
119
|
-
- 适合处理几 MB 到几百 MB 的文件
|
|
120
|
-
- 对于超大文件,建议添加进度监听(未来版本)
|
|
121
|
-
|
|
122
|
-
### Known Issues
|
|
123
|
-
|
|
124
|
-
- 仅支持 Android 平台(iOS 不需要此功能)
|
|
125
|
-
- 非常大的文件(> 500MB)可能需要较长时间
|
|
126
|
-
- 缓存文件需要手动清理(或等待系统清理)
|
|
127
|
-
|
|
128
|
-
### Credits
|
|
129
|
-
|
|
130
|
-
实现基于 Android ContentResolver API 和 Expo Modules 框架。
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# Content URI Handler - 实现总结
|
|
2
|
-
|
|
3
|
-
## 功能概述
|
|
4
|
-
|
|
5
|
-
为 `react-native-kookit` 添加了 Android ContentResolver 功能,用于处理来自其他应用的 `content://` URI,解决权限拒绝(Permission Denial)问题。
|
|
6
|
-
|
|
7
|
-
## 实现的文件
|
|
8
|
-
|
|
9
|
-
### 1. TypeScript 接口 (`src/ReactNativeKookitModule.ts`)
|
|
10
|
-
|
|
11
|
-
添加了两个新方法:
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
// 复制 content:// URI 到缓存目录
|
|
15
|
-
copyContentUriToCache(contentUri: string, fileName?: string): Promise<{
|
|
16
|
-
localPath: string;
|
|
17
|
-
fileName: string;
|
|
18
|
-
mimeType?: string;
|
|
19
|
-
size?: number;
|
|
20
|
-
}>;
|
|
21
|
-
|
|
22
|
-
// 获取 content:// URI 的元数据
|
|
23
|
-
getContentUriMetadata(contentUri: string): Promise<{
|
|
24
|
-
displayName?: string;
|
|
25
|
-
mimeType?: string;
|
|
26
|
-
size?: number;
|
|
27
|
-
}>;
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### 2. Android 原生实现 (`android/src/main/java/expo/modules/kookit/ReactNativeKookitModule.kt`)
|
|
31
|
-
|
|
32
|
-
#### 添加的导入
|
|
33
|
-
```kotlin
|
|
34
|
-
import android.net.Uri
|
|
35
|
-
import android.provider.OpenableColumns
|
|
36
|
-
import android.database.Cursor
|
|
37
|
-
import java.io.File
|
|
38
|
-
import java.io.FileOutputStream
|
|
39
|
-
import java.io.InputStream
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
#### 实现的功能
|
|
43
|
-
|
|
44
|
-
**`copyContentUriToCache`**
|
|
45
|
-
- 验证 URI scheme 是否为 `content://`
|
|
46
|
-
- 使用 `ContentResolver.openInputStream()` 读取文件流
|
|
47
|
-
- 将数据复制到应用缓存目录
|
|
48
|
-
- 使用 8KB 缓冲区提高效率
|
|
49
|
-
- 在 IO 线程执行,避免阻塞主线程
|
|
50
|
-
- 返回本地文件路径和元数据
|
|
51
|
-
|
|
52
|
-
**`getContentUriMetadata`**
|
|
53
|
-
- 使用 `ContentResolver.query()` 查询文件信息
|
|
54
|
-
- 获取 `DISPLAY_NAME` 和 `SIZE`
|
|
55
|
-
- 获取 MIME 类型
|
|
56
|
-
|
|
57
|
-
**辅助方法 `getContentUriMetadataInternal`**
|
|
58
|
-
- 共享的元数据查询逻辑
|
|
59
|
-
- 使用 Cursor 安全访问数据库
|
|
60
|
-
- 正确处理资源释放
|
|
61
|
-
|
|
62
|
-
### 3. 文档
|
|
63
|
-
|
|
64
|
-
- **`docs/CONTENT_URI_HANDLER.md`** - 完整的 API 文档和使用指南
|
|
65
|
-
- **`README.md`** - 更新了功能列表,添加了 Content URI Handler 介绍
|
|
66
|
-
|
|
67
|
-
### 4. 示例代码
|
|
68
|
-
|
|
69
|
-
- **`example/ContentUriExample.tsx`** - 完整的 React Native 示例组件
|
|
70
|
-
- 文件选择和复制
|
|
71
|
-
- 元数据查询
|
|
72
|
-
- UI 展示
|
|
73
|
-
- 错误处理
|
|
74
|
-
|
|
75
|
-
- **`example/ContentUriQuickExamples.ts`** - 快速参考示例
|
|
76
|
-
- 7 个常见使用场景
|
|
77
|
-
- 与 FTP 客户端集成
|
|
78
|
-
- 完整的工作流示例
|
|
79
|
-
|
|
80
|
-
## 技术亮点
|
|
81
|
-
|
|
82
|
-
### 1. 解决的核心问题
|
|
83
|
-
|
|
84
|
-
**问题:** 当通过"打开方式"或"分享"接收文件时,第三方应用可能没有正确设置 `FLAG_GRANT_READ_URI_PERMISSION`,导致:
|
|
85
|
-
```
|
|
86
|
-
java.lang.SecurityException: Permission Denial
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**解决方案:** 使用 `ContentResolver` 直接读取输入流,无需文件系统权限。
|
|
90
|
-
|
|
91
|
-
### 2. 优势
|
|
92
|
-
|
|
93
|
-
✅ **无需额外权限** - 不需要 `READ_EXTERNAL_STORAGE`
|
|
94
|
-
✅ **兼容性强** - 适用于所有 ContentProvider
|
|
95
|
-
✅ **性能优化** - 使用缓冲区,在后台线程执行
|
|
96
|
-
✅ **错误处理** - 完善的异常处理和资源清理
|
|
97
|
-
✅ **类型安全** - 完整的 TypeScript 类型定义
|
|
98
|
-
|
|
99
|
-
### 3. 实现细节
|
|
100
|
-
|
|
101
|
-
**线程管理:**
|
|
102
|
-
```kotlin
|
|
103
|
-
moduleScope.launch(Dispatchers.IO) {
|
|
104
|
-
// 文件操作在 IO 线程
|
|
105
|
-
withContext(Dispatchers.Main) {
|
|
106
|
-
// 回调在主线程
|
|
107
|
-
promise.resolve(result)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**资源管理:**
|
|
113
|
-
```kotlin
|
|
114
|
-
try {
|
|
115
|
-
inputStream = context.contentResolver.openInputStream(uri)
|
|
116
|
-
// ... 复制操作
|
|
117
|
-
} finally {
|
|
118
|
-
inputStream?.close()
|
|
119
|
-
outputStream?.close()
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**错误处理:**
|
|
124
|
-
```kotlin
|
|
125
|
-
if (uri.scheme != "content") {
|
|
126
|
-
throw Exception("Only content:// URIs are supported")
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## 使用场景
|
|
131
|
-
|
|
132
|
-
### 1. 文件选择器集成
|
|
133
|
-
```typescript
|
|
134
|
-
const result = await DocumentPicker.getDocumentAsync(...);
|
|
135
|
-
const file = await copyContentUriToCache(result.assets[0].uri);
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### 2. 处理 Intent
|
|
139
|
-
```typescript
|
|
140
|
-
const url = await Linking.getInitialURL();
|
|
141
|
-
if (url?.startsWith('content://')) {
|
|
142
|
-
const file = await copyContentUriToCache(url);
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### 3. 与 FTP/SMB 集成
|
|
147
|
-
```typescript
|
|
148
|
-
const file = await copyContentUriToCache(contentUri);
|
|
149
|
-
await ftpClient.upload(file.localPath, '/uploads/file.pdf');
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## 测试建议
|
|
153
|
-
|
|
154
|
-
### 单元测试
|
|
155
|
-
1. 验证 content:// URI 的处理
|
|
156
|
-
2. 测试元数据提取
|
|
157
|
-
3. 测试错误情况(无效 URI、权限问题等)
|
|
158
|
-
|
|
159
|
-
### 集成测试
|
|
160
|
-
1. 使用文件选择器选择文件
|
|
161
|
-
2. 通过"打开方式"分享文件到应用
|
|
162
|
-
3. 从不同的 ContentProvider 获取文件
|
|
163
|
-
4. 测试大文件(> 100MB)
|
|
164
|
-
|
|
165
|
-
### 性能测试
|
|
166
|
-
1. 不同文件大小的复制速度
|
|
167
|
-
2. 内存使用情况
|
|
168
|
-
3. 并发操作
|
|
169
|
-
|
|
170
|
-
## 兼容性
|
|
171
|
-
|
|
172
|
-
- **最低 Android 版本:** API 19 (Android 4.4)
|
|
173
|
-
- **React Native:** 0.60+
|
|
174
|
-
- **Expo:** SDK 44+
|
|
175
|
-
- **依赖:** 仅标准 Android API,无额外依赖
|
|
176
|
-
|
|
177
|
-
## 未来改进方向
|
|
178
|
-
|
|
179
|
-
1. **进度回调** - 为大文件添加复制进度
|
|
180
|
-
2. **流式处理** - 对于超大文件,提供流式访问而非完整复制
|
|
181
|
-
3. **缓存管理** - 自动清理旧的缓存文件
|
|
182
|
-
4. **批量操作** - 同时处理多个 content:// URI
|
|
183
|
-
5. **iOS 支持** - 为 iOS 添加类似功能(如果需要)
|
|
184
|
-
|
|
185
|
-
## API 变更
|
|
186
|
-
|
|
187
|
-
### 新增方法
|
|
188
|
-
|
|
189
|
-
- `copyContentUriToCache(contentUri: string, fileName?: string)`
|
|
190
|
-
- `getContentUriMetadata(contentUri: string)`
|
|
191
|
-
|
|
192
|
-
### 无破坏性变更
|
|
193
|
-
|
|
194
|
-
所有现有 API 保持不变,这是一个纯新增功能。
|
|
195
|
-
|
|
196
|
-
## 文档
|
|
197
|
-
|
|
198
|
-
- 📖 [完整 API 文档](./docs/CONTENT_URI_HANDLER.md)
|
|
199
|
-
- 💡 [快速示例](./example/ContentUriQuickExamples.ts)
|
|
200
|
-
- 🎨 [UI 示例](./example/ContentUriExample.tsx)
|
|
201
|
-
|
|
202
|
-
## 相关资源
|
|
203
|
-
|
|
204
|
-
- [Android ContentResolver 文档](https://developer.android.com/reference/android/content/ContentResolver)
|
|
205
|
-
- [Android 内容 URI 最佳实践](https://developer.android.com/training/secure-file-sharing)
|
|
206
|
-
- [Expo 文件系统](https://docs.expo.dev/versions/latest/sdk/filesystem/)
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A comprehensive React Native/Expo module with multiple utilities for Android and
|
|
|
14
14
|
The Content URI Handler solves a common Android problem: when receiving files via "Open with" or "Share" intents, apps receive `content://` URIs that often trigger `SecurityException: Permission Denial` errors.
|
|
15
15
|
|
|
16
16
|
**Key Benefits:**
|
|
17
|
+
|
|
17
18
|
- ✅ Bypass permission restrictions using Android's ContentResolver
|
|
18
19
|
- ✅ No `READ_EXTERNAL_STORAGE` permission needed
|
|
19
20
|
- ✅ Works with all third-party apps and file managers
|
package/TTS_CHECKLIST.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# TTS 功能实现检查清单
|
|
2
|
+
|
|
3
|
+
## ✅ 已完成的任务
|
|
4
|
+
|
|
5
|
+
### 1. TypeScript 类型定义
|
|
6
|
+
|
|
7
|
+
- [x] 在 `src/ReactNativeKookit.types.ts` 中添加 `TtsSynthesizeOptions` 类型
|
|
8
|
+
- [x] 在 `src/ReactNativeKookit.types.ts` 中添加 `TtsSynthesizeResult` 类型
|
|
9
|
+
- [x] 类型包含所有必需和可选参数
|
|
10
|
+
- [x] 添加了清晰的注释说明
|
|
11
|
+
|
|
12
|
+
### 2. TypeScript 模块接口
|
|
13
|
+
|
|
14
|
+
- [x] 在 `src/ReactNativeKookitModule.ts` 中导入 TTS 类型
|
|
15
|
+
- [x] 声明 `synthesizeSpeech` 方法
|
|
16
|
+
- [x] 添加 JSDoc 注释
|
|
17
|
+
- [x] 正确的类型签名和返回值
|
|
18
|
+
|
|
19
|
+
### 3. Android 原生实现
|
|
20
|
+
|
|
21
|
+
- [x] 导入必要的 Android TTS 相关类
|
|
22
|
+
- [x] 添加 TTS 实例变量
|
|
23
|
+
- [x] 实现 `synthesizeSpeech` AsyncFunction
|
|
24
|
+
- [x] TTS 初始化逻辑(异步等待)
|
|
25
|
+
- [x] 语言设置和验证
|
|
26
|
+
- [x] 语音ID支持(可选)
|
|
27
|
+
- [x] 音调和语速设置
|
|
28
|
+
- [x] 创建缓存目录 `tts`
|
|
29
|
+
- [x] 生成唯一文件名(UUID)
|
|
30
|
+
- [x] 合成音频到 WAV 文件
|
|
31
|
+
- [x] 实现 `parseLocale()` 辅助函数
|
|
32
|
+
- [x] 实现 `estimateDuration()` 辅助函数
|
|
33
|
+
- [x] 在 `onDestroy()` 中清理 TTS 资源
|
|
34
|
+
- [x] 完善的错误处理
|
|
35
|
+
|
|
36
|
+
### 4. iOS 原生实现
|
|
37
|
+
|
|
38
|
+
- [x] 添加 `AVSpeechSynthesizer` 实例变量
|
|
39
|
+
- [x] 实现 `synthesizeSpeech` AsyncFunction
|
|
40
|
+
- [x] 初始化语音合成器
|
|
41
|
+
- [x] 语言设置和语音选择
|
|
42
|
+
- [x] 支持语音ID(可选)
|
|
43
|
+
- [x] 音调和语速设置
|
|
44
|
+
- [x] 创建缓存目录 `tts`
|
|
45
|
+
- [x] 生成唯一文件名(UUID)
|
|
46
|
+
- [x] 使用 `AVSpeechSynthesizer.write()` 生成音频
|
|
47
|
+
- [x] 实现 `synthesizeSpeechToFile()` 核心方法
|
|
48
|
+
- [x] 实现 `writeBuffersToFile()` 音频写入方法
|
|
49
|
+
- [x] 实现 `estimateDuration()` 辅助函数
|
|
50
|
+
- [x] 使用 async/await 处理异步操作
|
|
51
|
+
- [x] 完善的错误处理
|
|
52
|
+
|
|
53
|
+
### 5. 导出和集成
|
|
54
|
+
|
|
55
|
+
- [x] `src/index.ts` 已通过 `export *` 自动导出 TTS 类型
|
|
56
|
+
- [x] 模块可以直接使用
|
|
57
|
+
|
|
58
|
+
### 6. 示例代码
|
|
59
|
+
|
|
60
|
+
- [x] 创建 `example/TtsExample.tsx` 完整示例
|
|
61
|
+
- [x] 包含文本输入界面
|
|
62
|
+
- [x] 语言选择功能
|
|
63
|
+
- [x] 音调和语速调整
|
|
64
|
+
- [x] 错误处理展示
|
|
65
|
+
- [x] 结果显示
|
|
66
|
+
- [x] 友好的用户界面
|
|
67
|
+
|
|
68
|
+
### 7. 文档
|
|
69
|
+
|
|
70
|
+
- [x] 创建 `docs/TTS_FEATURE.md` 完整文档
|
|
71
|
+
- API 说明
|
|
72
|
+
- 使用示例
|
|
73
|
+
- 平台详情
|
|
74
|
+
- 支持的语言
|
|
75
|
+
- 参数指南
|
|
76
|
+
- 错误处理
|
|
77
|
+
- 最佳实践
|
|
78
|
+
- 故障排除
|
|
79
|
+
|
|
80
|
+
- [x] 创建 `docs/TTS_QUICK_START.md` 快速入门
|
|
81
|
+
- 快速示例
|
|
82
|
+
- 常用语言代码
|
|
83
|
+
- 文件位置说明
|
|
84
|
+
|
|
85
|
+
- [x] 创建 `docs/TTS_IMPLEMENTATION_SUMMARY.md` 实现总结
|
|
86
|
+
- 技术细节
|
|
87
|
+
- 文件清单
|
|
88
|
+
- 后续改进建议
|
|
89
|
+
|
|
90
|
+
- [x] 创建 `docs/TTS_README_SECTION.md` README片段
|
|
91
|
+
- 可直接添加到主README
|
|
92
|
+
|
|
93
|
+
### 8. 测试和验证
|
|
94
|
+
|
|
95
|
+
- [x] TypeScript 编译通过
|
|
96
|
+
- [x] 无编译错误
|
|
97
|
+
- [x] 类型检查通过
|
|
98
|
+
- [x] 代码构建成功 (`npm run build`)
|
|
99
|
+
|
|
100
|
+
## 📋 功能特性验证
|
|
101
|
+
|
|
102
|
+
### 核心功能
|
|
103
|
+
|
|
104
|
+
- [x] 文本转语音合成
|
|
105
|
+
- [x] 支持自定义语言
|
|
106
|
+
- [x] 支持音调调整(0.5-2.0)
|
|
107
|
+
- [x] 支持语速调整(0.5-2.0)
|
|
108
|
+
- [x] 可选语音ID
|
|
109
|
+
- [x] 返回文件路径
|
|
110
|
+
- [x] 返回预估时长
|
|
111
|
+
|
|
112
|
+
### 平台支持
|
|
113
|
+
|
|
114
|
+
- [x] Android 实现(TextToSpeech API)
|
|
115
|
+
- [x] iOS 实现(AVSpeechSynthesizer)
|
|
116
|
+
- [x] 跨平台统一API
|
|
117
|
+
|
|
118
|
+
### 文件管理
|
|
119
|
+
|
|
120
|
+
- [x] 自动创建缓存目录
|
|
121
|
+
- [x] 唯一文件命名(UUID)
|
|
122
|
+
- [x] Android: WAV 格式
|
|
123
|
+
- [x] iOS: CAF 格式
|
|
124
|
+
|
|
125
|
+
### 开发体验
|
|
126
|
+
|
|
127
|
+
- [x] TypeScript 完整类型支持
|
|
128
|
+
- [x] Promise-based API
|
|
129
|
+
- [x] 清晰的错误消息
|
|
130
|
+
- [x] 详细的文档
|
|
131
|
+
- [x] 实用的示例
|
|
132
|
+
|
|
133
|
+
## 📁 文件清单
|
|
134
|
+
|
|
135
|
+
### 修改的文件(5个)
|
|
136
|
+
|
|
137
|
+
1. ✅ `src/ReactNativeKookit.types.ts` - TTS类型定义
|
|
138
|
+
2. ✅ `src/ReactNativeKookitModule.ts` - TTS方法声明
|
|
139
|
+
3. ✅ `android/src/main/java/expo/modules/kookit/ReactNativeKookitModule.kt` - Android实现
|
|
140
|
+
4. ✅ `ios/ReactNativeKookitModule.swift` - iOS实现
|
|
141
|
+
5. ✅ `src/index.ts` - 导出(已有 export \*)
|
|
142
|
+
|
|
143
|
+
### 新增的文件(5个)
|
|
144
|
+
|
|
145
|
+
1. ✅ `example/TtsExample.tsx` - 完整示例应用
|
|
146
|
+
2. ✅ `docs/TTS_FEATURE.md` - 完整文档
|
|
147
|
+
3. ✅ `docs/TTS_QUICK_START.md` - 快速入门
|
|
148
|
+
4. ✅ `docs/TTS_IMPLEMENTATION_SUMMARY.md` - 实现总结
|
|
149
|
+
5. ✅ `docs/TTS_README_SECTION.md` - README片段
|
|
150
|
+
|
|
151
|
+
## 🎯 API 使用示例
|
|
152
|
+
|
|
153
|
+
### 基础使用
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const result = await ReactNativeKookitModule.synthesizeSpeech({
|
|
157
|
+
text: "Hello World",
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 完整参数
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const result = await ReactNativeKookitModule.synthesizeSpeech({
|
|
165
|
+
text: "你好世界",
|
|
166
|
+
language: "zh-CN",
|
|
167
|
+
voiceId: "optional-voice-id",
|
|
168
|
+
pitch: 1.2,
|
|
169
|
+
rate: 0.9,
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 返回值
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
{
|
|
177
|
+
filePath: "/path/to/cache/tts/tts_uuid.wav", // Android
|
|
178
|
+
// or
|
|
179
|
+
filePath: "/path/to/cache/tts/tts_uuid.caf", // iOS
|
|
180
|
+
duration: 2.5 // seconds
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## ✨ 代码质量
|
|
185
|
+
|
|
186
|
+
- [x] 遵循项目代码风格
|
|
187
|
+
- [x] 使用 TypeScript 严格类型
|
|
188
|
+
- [x] 完善的错误处理
|
|
189
|
+
- [x] 清晰的变量命名
|
|
190
|
+
- [x] 详细的代码注释
|
|
191
|
+
- [x] 模块化设计
|
|
192
|
+
- [x] 资源清理(Android onDestroy)
|
|
193
|
+
|
|
194
|
+
## 🔍 测试状态
|
|
195
|
+
|
|
196
|
+
- [x] TypeScript 编译成功
|
|
197
|
+
- [x] 无语法错误
|
|
198
|
+
- [x] 无类型错误
|
|
199
|
+
- [x] Build 构建成功
|
|
200
|
+
- ⏳ 运行时测试(需要在设备上测试)
|
|
201
|
+
|
|
202
|
+
## 📦 输出文件
|
|
203
|
+
|
|
204
|
+
### Android
|
|
205
|
+
|
|
206
|
+
- **格式**: WAV (PCM)
|
|
207
|
+
- **位置**: `{cacheDir}/tts/`
|
|
208
|
+
- **命名**: `tts_{uuid}.wav`
|
|
209
|
+
- **API**: `TextToSpeech.synthesizeToFile()`
|
|
210
|
+
|
|
211
|
+
### iOS
|
|
212
|
+
|
|
213
|
+
- **格式**: CAF (Core Audio Format)
|
|
214
|
+
- **位置**: `{cachesDirectory}/tts/`
|
|
215
|
+
- **命名**: `tts_{uuid}.caf`
|
|
216
|
+
- **API**: `AVSpeechSynthesizer.write()`
|
|
217
|
+
|
|
218
|
+
## 🌍 语言支持
|
|
219
|
+
|
|
220
|
+
已文档化的常用语言:
|
|
221
|
+
|
|
222
|
+
- 英语(en-US, en-GB, en-AU)
|
|
223
|
+
- 中文(zh-CN, zh-TW, zh-HK)
|
|
224
|
+
- 西班牙语(es-ES, es-MX)
|
|
225
|
+
- 法语(fr-FR)
|
|
226
|
+
- 德语(de-DE)
|
|
227
|
+
- 日语(ja-JP)
|
|
228
|
+
- 韩语(ko-KR)
|
|
229
|
+
- 意大利语(it-IT)
|
|
230
|
+
- 葡萄牙语(pt-BR)
|
|
231
|
+
- 俄语(ru-RU)
|
|
232
|
+
- 阿拉伯语(ar-SA)
|
|
233
|
+
|
|
234
|
+
## 🚀 下一步
|
|
235
|
+
|
|
236
|
+
建议的后续步骤:
|
|
237
|
+
|
|
238
|
+
1. 在真实设备上测试 Android 版本
|
|
239
|
+
2. 在真实设备上测试 iOS 版本
|
|
240
|
+
3. 测试多种语言
|
|
241
|
+
4. 验证音频文件质量
|
|
242
|
+
5. 测试边界情况(长文本、特殊字符等)
|
|
243
|
+
6. 考虑添加单元测试
|
|
244
|
+
|
|
245
|
+
## ✅ 总结
|
|
246
|
+
|
|
247
|
+
TTS 功能已完全实现,包括:
|
|
248
|
+
|
|
249
|
+
- ✅ TypeScript 类型和接口定义
|
|
250
|
+
- ✅ Android 原生实现
|
|
251
|
+
- ✅ iOS 原生实现
|
|
252
|
+
- ✅ 完整的示例代码
|
|
253
|
+
- ✅ 详细的文档
|
|
254
|
+
- ✅ 编译通过,无错误
|
|
255
|
+
|
|
256
|
+
**状态**: 🎉 实现完成,可以使用!
|
|
@@ -17,6 +17,11 @@ import android.database.Cursor
|
|
|
17
17
|
import java.io.File
|
|
18
18
|
import java.io.FileOutputStream
|
|
19
19
|
import java.io.InputStream
|
|
20
|
+
import android.speech.tts.TextToSpeech
|
|
21
|
+
import android.speech.tts.UtteranceProgressListener
|
|
22
|
+
import android.os.Bundle
|
|
23
|
+
import java.util.Locale
|
|
24
|
+
import java.util.UUID
|
|
20
25
|
|
|
21
26
|
class ReactNativeKookitModule : Module() {
|
|
22
27
|
private var isVolumeListenerEnabled = false
|
|
@@ -26,6 +31,8 @@ class ReactNativeKookitModule : Module() {
|
|
|
26
31
|
private val ftpClients = mutableMapOf<String, FtpClient>()
|
|
27
32
|
private val smbClients = mutableMapOf<String, SmbClient>()
|
|
28
33
|
private val moduleScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
34
|
+
private var textToSpeech: TextToSpeech? = null
|
|
35
|
+
private var ttsInitialized = false
|
|
29
36
|
|
|
30
37
|
// Each module class must implement the definition function. The definition consists of components
|
|
31
38
|
// that describes the module's functionality and behavior.
|
|
@@ -812,6 +819,121 @@ class ReactNativeKookitModule : Module() {
|
|
|
812
819
|
}
|
|
813
820
|
}
|
|
814
821
|
|
|
822
|
+
// TTS (Text-to-Speech) Function
|
|
823
|
+
AsyncFunction("synthesizeSpeech") { options: Map<String, Any?>, promise: Promise ->
|
|
824
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
825
|
+
try {
|
|
826
|
+
val context = appContext.reactContext ?: throw Exception("React context is not available")
|
|
827
|
+
|
|
828
|
+
val text = options["text"] as? String ?: throw Exception("Text is required")
|
|
829
|
+
val voiceId = options["voiceId"] as? String
|
|
830
|
+
val language = options["language"] as? String ?: "en-US"
|
|
831
|
+
val pitch = (options["pitch"] as? Double)?.toFloat() ?: 1.0f
|
|
832
|
+
val rate = (options["rate"] as? Double)?.toFloat() ?: 1.0f
|
|
833
|
+
|
|
834
|
+
// Initialize TTS if not already initialized
|
|
835
|
+
if (textToSpeech == null) {
|
|
836
|
+
val latch = CompletableDeferred<Boolean>()
|
|
837
|
+
withContext(Dispatchers.Main) {
|
|
838
|
+
textToSpeech = TextToSpeech(context) { status ->
|
|
839
|
+
if (status == TextToSpeech.SUCCESS) {
|
|
840
|
+
ttsInitialized = true
|
|
841
|
+
latch.complete(true)
|
|
842
|
+
} else {
|
|
843
|
+
latch.completeExceptionally(Exception("TTS initialization failed"))
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
latch.await()
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Set language
|
|
851
|
+
val locale = parseLocale(language)
|
|
852
|
+
val result = textToSpeech?.setLanguage(locale)
|
|
853
|
+
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
|
854
|
+
throw Exception("Language not supported: $language")
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Set voice if specified
|
|
858
|
+
if (voiceId != null) {
|
|
859
|
+
val voices = textToSpeech?.voices
|
|
860
|
+
val voice = voices?.find { it.name == voiceId }
|
|
861
|
+
if (voice != null) {
|
|
862
|
+
textToSpeech?.voice = voice
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Set pitch and rate
|
|
867
|
+
textToSpeech?.setPitch(pitch)
|
|
868
|
+
textToSpeech?.setSpeechRate(rate)
|
|
869
|
+
|
|
870
|
+
// Create TTS cache directory
|
|
871
|
+
val ttsDir = File(context.cacheDir, "tts")
|
|
872
|
+
if (!ttsDir.exists()) {
|
|
873
|
+
ttsDir.mkdirs()
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Generate unique filename
|
|
877
|
+
val fileName = "tts_${UUID.randomUUID()}.wav"
|
|
878
|
+
val outputFile = File(ttsDir, fileName)
|
|
879
|
+
|
|
880
|
+
// Synthesize to file
|
|
881
|
+
val utteranceId = UUID.randomUUID().toString()
|
|
882
|
+
val synthesisLatch = CompletableDeferred<String>()
|
|
883
|
+
|
|
884
|
+
withContext<Unit>(Dispatchers.Main) {
|
|
885
|
+
textToSpeech?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
|
886
|
+
override fun onStart(uId: String?) {}
|
|
887
|
+
|
|
888
|
+
override fun onDone(id: String?) {
|
|
889
|
+
if (id == utteranceId) {
|
|
890
|
+
synthesisLatch.complete(outputFile.absolutePath)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
@Deprecated("Deprecated in Java", ReplaceWith("onError(String)"))
|
|
895
|
+
override fun onError(uId: String?) {
|
|
896
|
+
synthesisLatch.completeExceptionally(Exception("TTS synthesis failed"))
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
override fun onError(uId: String?, errorCode: Int) {
|
|
900
|
+
synthesisLatch.completeExceptionally(Exception("TTS synthesis failed with code: $errorCode"))
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
val params = Bundle().apply {
|
|
905
|
+
putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
@Suppress("DEPRECATION")
|
|
909
|
+
textToSpeech?.synthesizeToFile(text, params, outputFile, utteranceId)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
val filePath = synthesisLatch.await()
|
|
913
|
+
|
|
914
|
+
// Get file duration (approximate based on text length and rate)
|
|
915
|
+
val duration = estimateDuration(text, rate)
|
|
916
|
+
|
|
917
|
+
withContext(Dispatchers.Main) {
|
|
918
|
+
promise.resolve(mapOf(
|
|
919
|
+
"filePath" to filePath,
|
|
920
|
+
"duration" to duration
|
|
921
|
+
))
|
|
922
|
+
}
|
|
923
|
+
} catch (e: Exception) {
|
|
924
|
+
withContext(Dispatchers.Main) {
|
|
925
|
+
promise.reject("TTS_SYNTHESIS_ERROR", "Failed to synthesize speech: ${e.message}", e)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Cleanup when module is destroyed
|
|
932
|
+
OnDestroy {
|
|
933
|
+
textToSpeech?.shutdown()
|
|
934
|
+
textToSpeech = null
|
|
935
|
+
}
|
|
936
|
+
|
|
815
937
|
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
816
938
|
// the view definition: Prop, Events.
|
|
817
939
|
View(ReactNativeKookitView::class) {
|
|
@@ -928,4 +1050,27 @@ class ReactNativeKookitModule : Module() {
|
|
|
928
1050
|
cursor?.close()
|
|
929
1051
|
}
|
|
930
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Parse locale string (e.g., "en-US", "zh-CN") to Locale object
|
|
1056
|
+
*/
|
|
1057
|
+
private fun parseLocale(localeString: String): Locale {
|
|
1058
|
+
val parts = localeString.split("-", "_")
|
|
1059
|
+
return when (parts.size) {
|
|
1060
|
+
1 -> Locale(parts[0])
|
|
1061
|
+
2 -> Locale(parts[0], parts[1])
|
|
1062
|
+
else -> Locale(parts[0], parts[1], parts[2])
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Estimate audio duration based on text length and speech rate
|
|
1068
|
+
* This is a rough estimation: average speaking rate is about 150 words per minute
|
|
1069
|
+
*/
|
|
1070
|
+
private fun estimateDuration(text: String, rate: Float): Double {
|
|
1071
|
+
val wordCount = text.split("\\s+".toRegex()).size
|
|
1072
|
+
val baseMinutes = wordCount / 150.0
|
|
1073
|
+
val durationSeconds = (baseMinutes * 60.0) / rate
|
|
1074
|
+
return durationSeconds
|
|
1075
|
+
}
|
|
931
1076
|
}
|
|
@@ -96,4 +96,15 @@ export type SmbEventPayload = {
|
|
|
96
96
|
onComplete?: () => void;
|
|
97
97
|
onError?: (error: string) => void;
|
|
98
98
|
};
|
|
99
|
+
export type TtsSynthesizeOptions = {
|
|
100
|
+
text: string;
|
|
101
|
+
voiceId?: string;
|
|
102
|
+
language?: string;
|
|
103
|
+
pitch?: number;
|
|
104
|
+
rate?: number;
|
|
105
|
+
};
|
|
106
|
+
export type TtsSynthesizeResult = {
|
|
107
|
+
filePath: string;
|
|
108
|
+
duration?: number;
|
|
109
|
+
};
|
|
99
110
|
//# sourceMappingURL=ReactNativeKookit.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeKookit.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeKookit.types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReactNativeKookit.types.d.ts","sourceRoot":"","sources":["../src/ReactNativeKookit.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeKookit.types.js","sourceRoot":"","sources":["../src/ReactNativeKookit.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from \"react-native\";\n\nexport type OnLoadEventPayload = {\n url: string;\n};\n\nexport type VolumeKeyEventPayload = {\n key: \"up\" | \"down\";\n};\n\nexport type FtpConnectionConfig = {\n host: string;\n port?: number;\n username: string;\n password: string;\n passive?: boolean;\n timeout?: number;\n};\n\nexport type FtpFileInfo = {\n name: string;\n isDirectory: boolean;\n size: number;\n lastModified: string;\n permissions?: string;\n};\n\nexport type FtpProgressInfo = {\n transferred: number;\n total: number;\n percentage: number;\n};\n\nexport type FtpProgressEvent = FtpProgressInfo & {\n clientId: string;\n};\n\nexport type FtpCompleteEvent = {\n clientId: string;\n};\n\nexport type FtpErrorEvent = {\n clientId: string;\n error: string;\n};\n\nexport type FtpEventPayload = {\n onProgress?: (progress: FtpProgressInfo) => void;\n onComplete?: () => void;\n onError?: (error: string) => void;\n};\n\nexport type ReactNativeKookitModuleEvents = {\n onChange: (params: ChangeEventPayload) => void;\n onVolumeButtonPressed: (params: VolumeKeyEventPayload) => void;\n onFtpProgress: (params: FtpProgressEvent) => void;\n onFtpComplete: (params: FtpCompleteEvent) => void;\n onFtpError: (params: FtpErrorEvent) => void;\n // SMB events (reserved for future file operations parity)\n onSmbProgress: (params: SmbProgressEvent) => void;\n onSmbComplete: (params: SmbCompleteEvent) => void;\n onSmbError: (params: SmbErrorEvent) => void;\n};\n\nexport type ChangeEventPayload = {\n value: string;\n};\n\nexport type ReactNativeKookitViewProps = {\n url: string;\n onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;\n style?: StyleProp<ViewStyle>;\n};\n\n// SMB API parity with FTP (OOP new-client style)\nexport type SmbConnectionConfig = {\n host: string;\n port?: number; // default 445\n username: string;\n password: string;\n domain?: string; // optional Windows domain or workgroup\n share?: string; // optional default share to open\n timeout?: number; // ms\n};\n\nexport type SmbFileInfo = {\n name: string;\n isDirectory: boolean;\n size: number;\n lastModified?: string;\n attributes?: string;\n};\n\nexport type SmbProgressInfo = {\n transferred: number;\n total: number;\n percentage: number;\n};\n\nexport type SmbProgressEvent = SmbProgressInfo & {\n clientId: string;\n};\n\nexport type SmbCompleteEvent = {\n clientId: string;\n};\n\nexport type SmbErrorEvent = {\n clientId: string;\n error: string;\n};\n\nexport type SmbEventPayload = {\n onProgress?: (progress: SmbProgressInfo) => void;\n onComplete?: () => void;\n onError?: (error: string) => void;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"ReactNativeKookit.types.js","sourceRoot":"","sources":["../src/ReactNativeKookit.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from \"react-native\";\n\nexport type OnLoadEventPayload = {\n url: string;\n};\n\nexport type VolumeKeyEventPayload = {\n key: \"up\" | \"down\";\n};\n\nexport type FtpConnectionConfig = {\n host: string;\n port?: number;\n username: string;\n password: string;\n passive?: boolean;\n timeout?: number;\n};\n\nexport type FtpFileInfo = {\n name: string;\n isDirectory: boolean;\n size: number;\n lastModified: string;\n permissions?: string;\n};\n\nexport type FtpProgressInfo = {\n transferred: number;\n total: number;\n percentage: number;\n};\n\nexport type FtpProgressEvent = FtpProgressInfo & {\n clientId: string;\n};\n\nexport type FtpCompleteEvent = {\n clientId: string;\n};\n\nexport type FtpErrorEvent = {\n clientId: string;\n error: string;\n};\n\nexport type FtpEventPayload = {\n onProgress?: (progress: FtpProgressInfo) => void;\n onComplete?: () => void;\n onError?: (error: string) => void;\n};\n\nexport type ReactNativeKookitModuleEvents = {\n onChange: (params: ChangeEventPayload) => void;\n onVolumeButtonPressed: (params: VolumeKeyEventPayload) => void;\n onFtpProgress: (params: FtpProgressEvent) => void;\n onFtpComplete: (params: FtpCompleteEvent) => void;\n onFtpError: (params: FtpErrorEvent) => void;\n // SMB events (reserved for future file operations parity)\n onSmbProgress: (params: SmbProgressEvent) => void;\n onSmbComplete: (params: SmbCompleteEvent) => void;\n onSmbError: (params: SmbErrorEvent) => void;\n};\n\nexport type ChangeEventPayload = {\n value: string;\n};\n\nexport type ReactNativeKookitViewProps = {\n url: string;\n onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void;\n style?: StyleProp<ViewStyle>;\n};\n\n// SMB API parity with FTP (OOP new-client style)\nexport type SmbConnectionConfig = {\n host: string;\n port?: number; // default 445\n username: string;\n password: string;\n domain?: string; // optional Windows domain or workgroup\n share?: string; // optional default share to open\n timeout?: number; // ms\n};\n\nexport type SmbFileInfo = {\n name: string;\n isDirectory: boolean;\n size: number;\n lastModified?: string;\n attributes?: string;\n};\n\nexport type SmbProgressInfo = {\n transferred: number;\n total: number;\n percentage: number;\n};\n\nexport type SmbProgressEvent = SmbProgressInfo & {\n clientId: string;\n};\n\nexport type SmbCompleteEvent = {\n clientId: string;\n};\n\nexport type SmbErrorEvent = {\n clientId: string;\n error: string;\n};\n\nexport type SmbEventPayload = {\n onProgress?: (progress: SmbProgressInfo) => void;\n onComplete?: () => void;\n onError?: (error: string) => void;\n};\n\n// TTS (Text-to-Speech) API\nexport type TtsSynthesizeOptions = {\n text: string;\n voiceId?: string; // Voice identifier (platform-specific)\n language?: string; // Language code (e.g., 'en-US', 'zh-CN')\n pitch?: number; // Pitch (0.5 - 2.0, default: 1.0)\n rate?: number; // Speech rate (0.5 - 2.0, default: 1.0)\n};\n\nexport type TtsSynthesizeResult = {\n filePath: string; // Path to the generated audio file\n duration?: number; // Duration in seconds (if available)\n};\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NativeModule } from "expo";
|
|
2
|
-
import { ReactNativeKookitModuleEvents, FtpConnectionConfig, FtpFileInfo, SmbConnectionConfig, SmbFileInfo } from "./ReactNativeKookit.types";
|
|
2
|
+
import { ReactNativeKookitModuleEvents, FtpConnectionConfig, FtpFileInfo, SmbConnectionConfig, SmbFileInfo, TtsSynthesizeOptions, TtsSynthesizeResult } from "./ReactNativeKookit.types";
|
|
3
3
|
declare class ReactNativeKookitModule extends NativeModule<ReactNativeKookitModuleEvents> {
|
|
4
4
|
PI: number;
|
|
5
5
|
/**
|
|
@@ -169,6 +169,14 @@ declare class ReactNativeKookitModule extends NativeModule<ReactNativeKookitModu
|
|
|
169
169
|
mimeType?: string;
|
|
170
170
|
size?: number;
|
|
171
171
|
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Synthesize speech from text and save to audio file.
|
|
174
|
+
* The audio file will be saved in the cache directory under 'tts' folder.
|
|
175
|
+
*
|
|
176
|
+
* @param options TTS synthesis options including text, voiceId, language, pitch, and rate
|
|
177
|
+
* @returns Promise that resolves with file path and duration of the generated audio
|
|
178
|
+
*/
|
|
179
|
+
synthesizeSpeech(options: TtsSynthesizeOptions): Promise<TtsSynthesizeResult>;
|
|
172
180
|
}
|
|
173
181
|
declare const _default: ReactNativeKookitModule;
|
|
174
182
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeKookitModule.d.ts","sourceRoot":"","sources":["../src/ReactNativeKookitModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,6BAA6B,EAC7B,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,WAAW,
|
|
1
|
+
{"version":3,"file":"ReactNativeKookitModule.d.ts","sourceRoot":"","sources":["../src/ReactNativeKookitModule.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeKookitModule.js","sourceRoot":"","sources":["../src/ReactNativeKookitModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ReactNativeKookitModule.js","sourceRoot":"","sources":["../src/ReactNativeKookitModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA+OzD,yDAAyD;AACzD,eAAe,mBAAmB,CAChC,mBAAmB,CACpB,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nimport {\n ReactNativeKookitModuleEvents,\n FtpConnectionConfig,\n FtpFileInfo,\n SmbConnectionConfig,\n SmbFileInfo,\n TtsSynthesizeOptions,\n TtsSynthesizeResult,\n} from \"./ReactNativeKookit.types\";\n\ndeclare class ReactNativeKookitModule extends NativeModule<ReactNativeKookitModuleEvents> {\n PI: number;\n\n /**\n * Returns a hello world string\n */\n hello(): string;\n\n /**\n * Test async function that sends a change event\n */\n setValueAsync(value: string): Promise<void>;\n\n /**\n * Enables volume key interception.\n * On Android, your MainActivity must implement VolumeKeyInterceptActivity interface.\n * On iOS, this works automatically.\n *\n * @throws Error if MainActivity doesn't implement VolumeKeyInterceptActivity on Android\n */\n enableVolumeKeyInterception(): void;\n\n /**\n * Disables volume key interception\n */\n disableVolumeKeyInterception(): void;\n\n // New FTP Client API Methods\n\n /**\n * Create a new FTP client instance\n * @param clientId Unique identifier for the FTP client\n * @returns Promise that resolves with client creation result\n */\n createFtpClient(clientId: string): Promise<{ clientId: string }>;\n\n /**\n * Connect FTP client to server\n * @param clientId FTP client identifier\n * @param config FTP connection configuration\n * @returns Promise that resolves when connected\n */\n ftpClientConnect(\n clientId: string,\n config: FtpConnectionConfig\n ): Promise<void>;\n\n /**\n * Disconnect FTP client from server\n * @param clientId FTP client identifier\n * @returns Promise that resolves when disconnected\n */\n ftpClientDisconnect(clientId: string): Promise<void>;\n\n /**\n * Dispose FTP client and clean up resources\n * @param clientId FTP client identifier\n * @returns Promise that resolves when disposed\n */\n disposeFtpClient(clientId: string): Promise<void>;\n\n /**\n * List files and directories\n * @param clientId FTP client identifier\n * @param path Optional path to list (defaults to current directory)\n * @returns Promise that resolves with array of file information\n */\n ftpClientList(clientId: string, path?: string): Promise<FtpFileInfo[]>;\n\n /**\n * Download a file from FTP server\n * @param clientId FTP client identifier\n * @param remotePath Remote file path on FTP server\n * @param localPath Local file path to save the downloaded file\n * @returns Promise that resolves when download is complete\n */\n ftpClientDownload(\n clientId: string,\n remotePath: string,\n localPath: string\n ): Promise<void>;\n\n /**\n * Upload a file to FTP server\n * @param clientId FTP client identifier\n * @param localPath Local file path to upload\n * @param remotePath Remote file path on FTP server\n * @returns Promise that resolves when upload is complete\n */\n ftpClientUpload(\n clientId: string,\n localPath: string,\n remotePath: string\n ): Promise<void>;\n\n /**\n * Delete a file or directory on FTP server\n * @param clientId FTP client identifier\n * @param remotePath Remote file or directory path to delete\n * @param isDirectory Whether the path is a directory (default: false)\n * @returns Promise that resolves when deletion is complete\n */\n ftpClientDelete(\n clientId: string,\n remotePath: string,\n isDirectory?: boolean\n ): Promise<void>;\n\n /**\n * Create a directory on FTP server\n * @param clientId FTP client identifier\n * @param remotePath Remote directory path to create\n * @returns Promise that resolves when directory is created\n */\n ftpClientCreateDirectory(clientId: string, remotePath: string): Promise<void>;\n\n /**\n * Change current working directory on FTP server\n * @param clientId FTP client identifier\n * @param remotePath Remote directory path to change to\n * @returns Promise that resolves when directory is changed\n */\n ftpClientChangeDirectory(clientId: string, remotePath: string): Promise<void>;\n\n /**\n * Get current working directory on FTP server\n * @param clientId FTP client identifier\n * @returns Promise that resolves with current directory path\n */\n ftpClientGetCurrentDirectory(clientId: string): Promise<string>;\n\n /**\n * Get FTP client status\n * @param clientId FTP client identifier\n * @returns Client status information\n */\n getFtpClientStatus(\n clientId: string\n ): Promise<{ exists: boolean; connected: boolean }>;\n\n /**\n * List all FTP clients\n * @returns Object containing all clients and their status\n */\n listFtpClients(): Promise<{\n clients: Record<string, { connected: boolean }>;\n count: number;\n }>;\n\n // SMB Client API\n\n createSmbClient(clientId: string): Promise<{ clientId: string }>;\n smbClientConnect(\n clientId: string,\n config: SmbConnectionConfig\n ): Promise<void>;\n smbClientConnectShare(clientId: string, shareName: string): Promise<void>;\n smbClientDisconnect(clientId: string): Promise<void>;\n disposeSmbClient(clientId: string): Promise<void>;\n smbClientList(clientId: string, path?: string): Promise<SmbFileInfo[]>;\n smbClientDownload(\n clientId: string,\n remotePath: string,\n localPath: string\n ): Promise<void>;\n smbClientUpload(\n clientId: string,\n localPath: string,\n remotePath: string\n ): Promise<void>;\n smbClientDelete(\n clientId: string,\n remotePath: string,\n isDirectory?: boolean\n ): Promise<void>;\n smbClientCreateDirectory(clientId: string, remotePath: string): Promise<void>;\n getSmbClientStatus(\n clientId: string\n ): Promise<{ exists: boolean; connected: boolean }>;\n listSmbClients(): Promise<{\n clients: Record<string, { connected: boolean }>;\n count: number;\n }>;\n\n /**\n * Copy a file from content:// URI to local cache directory.\n * This method uses Android's ContentResolver to read the file stream,\n * bypassing permission restrictions when receiving content URIs from other apps.\n *\n * @param contentUri The content:// URI received from another app (e.g., via Intent)\n * @param fileName Optional filename for the destination file. If not provided, tries to extract from URI\n * @returns Promise that resolves with the local file path (file://)\n * @platform android\n */\n copyContentUriToCache(\n contentUri: string,\n fileName?: string\n ): Promise<{\n localPath: string;\n fileName: string;\n mimeType?: string;\n size?: number;\n }>;\n\n /**\n * Get metadata about a content:// URI without copying the file.\n *\n * @param contentUri The content:// URI to query\n * @returns Promise that resolves with file metadata\n * @platform android\n */\n getContentUriMetadata(contentUri: string): Promise<{\n displayName?: string;\n mimeType?: string;\n size?: number;\n }>;\n\n /**\n * Synthesize speech from text and save to audio file.\n * The audio file will be saved in the cache directory under 'tts' folder.\n *\n * @param options TTS synthesis options including text, voiceId, language, pitch, and rate\n * @returns Promise that resolves with file path and duration of the generated audio\n */\n synthesizeSpeech(options: TtsSynthesizeOptions): Promise<TtsSynthesizeResult>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ReactNativeKookitModule>(\n \"ReactNativeKookit\"\n);\n"]}
|
|
@@ -1022,6 +1022,8 @@ public class ReactNativeKookitModule: Module {
|
|
|
1022
1022
|
private var previousVolume: Float = 0.0
|
|
1023
1023
|
private var ftpClients: [String: FtpClient] = [:] // Store multiple FTP clients by ID
|
|
1024
1024
|
private var smbClients: [String: SmbClient] = [:] // Store multiple SMB clients by ID
|
|
1025
|
+
private var speechSynthesizer: AVSpeechSynthesizer?
|
|
1026
|
+
private var currentSynthesisTask: Task<String, Error>?
|
|
1025
1027
|
|
|
1026
1028
|
// Each module class must implement the definition function. The definition consists of components
|
|
1027
1029
|
// that describes the module's functionality and behavior.
|
|
@@ -1547,6 +1549,47 @@ public class ReactNativeKookitModule: Module {
|
|
|
1547
1549
|
}
|
|
1548
1550
|
}
|
|
1549
1551
|
|
|
1552
|
+
// TTS (Text-to-Speech) Function
|
|
1553
|
+
AsyncFunction("synthesizeSpeech") { (options: [String: Any], promise: Promise) in
|
|
1554
|
+
Task {
|
|
1555
|
+
do {
|
|
1556
|
+
let text = options["text"] as? String ?? ""
|
|
1557
|
+
if text.isEmpty {
|
|
1558
|
+
promise.reject("TTS_ERROR", "Text is required")
|
|
1559
|
+
return
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
let voiceId = options["voiceId"] as? String
|
|
1563
|
+
let language = options["language"] as? String ?? "en-US"
|
|
1564
|
+
let pitch = options["pitch"] as? Double ?? 1.0
|
|
1565
|
+
let rate = options["rate"] as? Double ?? 1.0
|
|
1566
|
+
|
|
1567
|
+
// Initialize synthesizer if needed
|
|
1568
|
+
if self.speechSynthesizer == nil {
|
|
1569
|
+
self.speechSynthesizer = AVSpeechSynthesizer()
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
let filePath = try await self.synthesizeSpeechToFile(
|
|
1573
|
+
text: text,
|
|
1574
|
+
voiceId: voiceId,
|
|
1575
|
+
language: language,
|
|
1576
|
+
pitch: Float(pitch),
|
|
1577
|
+
rate: Float(rate)
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
// Estimate duration
|
|
1581
|
+
let duration = self.estimateDuration(text: text, rate: Float(rate))
|
|
1582
|
+
|
|
1583
|
+
promise.resolve([
|
|
1584
|
+
"filePath": filePath,
|
|
1585
|
+
"duration": duration
|
|
1586
|
+
])
|
|
1587
|
+
} catch {
|
|
1588
|
+
promise.reject("TTS_SYNTHESIS_ERROR", error.localizedDescription)
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1550
1593
|
// Enables the module to be used as a native view. Definition components that are accepted as part of
|
|
1551
1594
|
// the view definition: Prop, Events.
|
|
1552
1595
|
View(ReactNativeKookitView.self) {
|
|
@@ -1640,6 +1683,106 @@ public class ReactNativeKookitModule: Module {
|
|
|
1640
1683
|
}
|
|
1641
1684
|
}
|
|
1642
1685
|
}
|
|
1686
|
+
|
|
1687
|
+
// MARK: - TTS Helper Methods
|
|
1688
|
+
|
|
1689
|
+
private func synthesizeSpeechToFile(text: String, voiceId: String?, language: String, pitch: Float, rate: Float) async throws -> String {
|
|
1690
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
1691
|
+
guard let synthesizer = self.speechSynthesizer else {
|
|
1692
|
+
continuation.resume(throwing: NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Speech synthesizer not initialized"]))
|
|
1693
|
+
return
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
let utterance = AVSpeechUtterance(string: text)
|
|
1697
|
+
|
|
1698
|
+
// Set voice
|
|
1699
|
+
if let voiceId = voiceId {
|
|
1700
|
+
utterance.voice = AVSpeechSynthesisVoice(identifier: voiceId)
|
|
1701
|
+
} else {
|
|
1702
|
+
// Try to find voice by language
|
|
1703
|
+
let voices = AVSpeechSynthesisVoice.speechVoices()
|
|
1704
|
+
if let voice = voices.first(where: { $0.language.hasPrefix(language) }) {
|
|
1705
|
+
utterance.voice = voice
|
|
1706
|
+
} else {
|
|
1707
|
+
utterance.voice = AVSpeechSynthesisVoice(language: language)
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Set pitch and rate
|
|
1712
|
+
utterance.pitchMultiplier = pitch
|
|
1713
|
+
utterance.rate = rate * AVSpeechUtteranceDefaultSpeechRate
|
|
1714
|
+
|
|
1715
|
+
// Create TTS cache directory
|
|
1716
|
+
let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
1717
|
+
let ttsDir = cacheDir.appendingPathComponent("tts")
|
|
1718
|
+
|
|
1719
|
+
do {
|
|
1720
|
+
try FileManager.default.createDirectory(at: ttsDir, withIntermediateDirectories: true, attributes: nil)
|
|
1721
|
+
} catch {
|
|
1722
|
+
continuation.resume(throwing: error)
|
|
1723
|
+
return
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Generate unique filename
|
|
1727
|
+
let fileName = "tts_\(UUID().uuidString).caf"
|
|
1728
|
+
let outputURL = ttsDir.appendingPathComponent(fileName)
|
|
1729
|
+
|
|
1730
|
+
// Write utterance to file using AVSpeechSynthesizer.write
|
|
1731
|
+
Task {
|
|
1732
|
+
do {
|
|
1733
|
+
var audioBuffers: [AVAudioPCMBuffer] = []
|
|
1734
|
+
|
|
1735
|
+
synthesizer.write(utterance) { buffer in
|
|
1736
|
+
if let buffer = buffer as? AVAudioPCMBuffer {
|
|
1737
|
+
audioBuffers.append(buffer)
|
|
1738
|
+
} else if buffer == nil {
|
|
1739
|
+
// Writing is complete when buffer is nil
|
|
1740
|
+
Task {
|
|
1741
|
+
do {
|
|
1742
|
+
// Write all buffers to file
|
|
1743
|
+
try self.writeBuffersToFile(buffers: audioBuffers, url: outputURL)
|
|
1744
|
+
continuation.resume(returning: outputURL.path)
|
|
1745
|
+
} catch {
|
|
1746
|
+
continuation.resume(throwing: error)
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
private func writeBuffersToFile(buffers: [AVAudioPCMBuffer], url: URL) throws {
|
|
1757
|
+
guard let firstBuffer = buffers.first else {
|
|
1758
|
+
throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "No audio data generated"])
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Create audio file with settings from the buffer
|
|
1762
|
+
let settings: [String: Any] = [
|
|
1763
|
+
AVFormatIDKey: kAudioFormatLinearPCM,
|
|
1764
|
+
AVSampleRateKey: firstBuffer.format.sampleRate,
|
|
1765
|
+
AVNumberOfChannelsKey: firstBuffer.format.channelCount,
|
|
1766
|
+
AVLinearPCMBitDepthKey: 16,
|
|
1767
|
+
AVLinearPCMIsFloatKey: false,
|
|
1768
|
+
AVLinearPCMIsBigEndianKey: false
|
|
1769
|
+
]
|
|
1770
|
+
|
|
1771
|
+
let audioFile = try AVAudioFile(forWriting: url, settings: settings)
|
|
1772
|
+
|
|
1773
|
+
// Write all buffers to file
|
|
1774
|
+
for buffer in buffers {
|
|
1775
|
+
try audioFile.write(from: buffer)
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
private func estimateDuration(text: String, rate: Float) -> Double {
|
|
1780
|
+
// Average speaking rate is about 150 words per minute
|
|
1781
|
+
let words = text.split(separator: " ").count
|
|
1782
|
+
let baseMinutes = Double(words) / 150.0
|
|
1783
|
+
let durationSeconds = (baseMinutes * 60.0) / Double(rate)
|
|
1784
|
+
return durationSeconds
|
|
1785
|
+
}
|
|
1643
1786
|
}
|
|
1644
1787
|
|
|
1645
1788
|
// Helper class to handle FTP progress callbacks
|
package/package.json
CHANGED