bump version to 1.8.2
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**是一款面向桌面端的 AI 视频创作应用,采用"Python 后端 API + Tauri 桌面前端"的混合架构。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video` / `cn.meijiaka.ai-zy`
|
||||
- **版本**: `1.8.1`
|
||||
- **版本**: `1.8.2`
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成(TTS)、声音复刻、视频生成(Vidu)、视频字幕生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
### 技术栈总览
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# === 基础配置 ===
|
||||
APP_NAME=美家卡智影 API
|
||||
APP_VERSION=1.8.1
|
||||
APP_VERSION=1.8.2
|
||||
# ⚠️ 生产环境必须设为 false
|
||||
DEBUG=true
|
||||
ENV=development
|
||||
|
||||
@@ -24,7 +24,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = Field(default="美家卡智影 API", description="应用名称")
|
||||
APP_VERSION: str = Field(default="1.8.1", description="应用版本")
|
||||
APP_VERSION: str = Field(default="1.8.2", description="应用版本")
|
||||
DEBUG: bool = Field(default=False, description="调试模式")
|
||||
ENV: Literal["development", "staging", "production"] = Field(
|
||||
default="development", description="运行环境"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
description = "美家卡智影 - AI 视频创作后端 API"
|
||||
authors = [{ name = "Meijiaka Team" }]
|
||||
readme = "README.md"
|
||||
|
||||
Generated
+1
-1
@@ -944,7 +944,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meijiaka-ai-api"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
**美家卡智影**(产品名)是一款基于 Tauri v2 + React 19 + TypeScript 的桌面端 AI 视频创作应用。
|
||||
|
||||
- **产品标识**: `cn.meijiaka.ai-video`
|
||||
- **版本**: `1.8.1`
|
||||
- **版本**: `1.8.2`
|
||||
- **窗口尺寸**: 1200×800,不可缩放(`resizable: false`)
|
||||
- **核心功能**: AI 脚本生成、AI 配音合成、视频生成、压制成片(FFmpeg)、项目本地持久化
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tauri-app",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"private": true,
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
Generated
+1
-1
@@ -4219,7 +4219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-app"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-app"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
description = "美家卡智影 - AI 视频创作桌面应用"
|
||||
authors = ["美家卡科技"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -55,20 +55,24 @@ fn validate_nine_sixteen_aspect(width: u32, height: u32) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据素材最小高度决定目标输出分辨率
|
||||
/// 根据素材最大高度决定目标输出分辨率
|
||||
///
|
||||
/// 向上统一到最高素材的分辨率:只要存在 1080p 素材,成片就输出 1080p,
|
||||
/// 避免高分辨率素材被强制压缩到 720p 导致画面模糊。
|
||||
/// 720p 素材会被放大拼接,虽然略有损失,但优于整体降质。
|
||||
fn resolve_target_resolution(heights: &[u32]) -> Option<(u32, u32)> {
|
||||
let min_height = heights.iter().min()?;
|
||||
if *min_height <= 1280 {
|
||||
Some((720, 1280))
|
||||
} else {
|
||||
let max_height = heights.iter().max()?;
|
||||
if *max_height >= 1920 {
|
||||
Some((1080, 1920))
|
||||
} else {
|
||||
Some((720, 1280))
|
||||
}
|
||||
}
|
||||
|
||||
/// 拼接视频片段
|
||||
///
|
||||
/// 1. 探测所有片段分辨率并校验 9:16 比例
|
||||
/// 2. 按最小高度决定目标输出分辨率
|
||||
/// 2. 按最大高度决定目标输出分辨率(向上统一,避免高分辨率素材被压糊)
|
||||
/// 3. 统一标准化后拼接
|
||||
#[tauri::command]
|
||||
pub async fn concat_video_clips(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "美家卡智影",
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.2",
|
||||
"identifier": "cn.meijiaka.ai-zy",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function VoiceMaterialLibrary() {
|
||||
// 音频文件验证
|
||||
const validateAudioFile = (file: File): Promise<{ valid: boolean; error?: string }> => {
|
||||
return new Promise(resolve => {
|
||||
const maxSize = 20 * 1024 * 1024; // 20MB
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||||
if (file.size > maxSize) {
|
||||
resolve({ valid: false, error: `文件大小 ${(file.size / 1024 / 1024).toFixed(1)}MB,要求不超过 20MB` });
|
||||
return;
|
||||
@@ -260,6 +260,7 @@ export default function VoiceMaterialLibrary() {
|
||||
progress.update('正在生成专属音色...');
|
||||
progress.success('复刻成功', 200);
|
||||
} catch (err) {
|
||||
console.error('[VoiceMaterialLibrary] handleUpload catch:', err, '类型:', typeof err, '是Error?', err instanceof Error);
|
||||
const msg = err instanceof Error ? err.message : '上传失败';
|
||||
if (msg.includes('402') || msg.includes('积分不足')) {
|
||||
setShowPointsModal(true);
|
||||
@@ -419,7 +420,7 @@ export default function VoiceMaterialLibrary() {
|
||||
<div style={{ fontSize: 'var(--font-sm)' }}>点击选择文件</div>
|
||||
<div style={{ fontSize: 'var(--font-xs)', marginTop: 6, lineHeight: 1.6 }}>
|
||||
<div>支持 MP3 / M4A / WAV / MP4</div>
|
||||
<div>人声干净无杂音,时长 10 秒 ~ 2 分钟,不超过 20MB</div>
|
||||
<div>人声干净无杂音,时长 10 秒 ~ 2 分钟,不超过 50MB</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { create } from 'zustand';
|
||||
import type { VoiceInfo, VoiceMaterial } from '../api/modules/voice';
|
||||
import * as voiceApi from '../api/modules/voice';
|
||||
import { writeFile, remove, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
import { writeFile, remove, mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||
import { join, appLocalDataDir } from '@tauri-apps/api/path';
|
||||
|
||||
interface VoiceState {
|
||||
@@ -136,33 +136,45 @@ export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
|
||||
try {
|
||||
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
||||
console.log('[VoiceStore] addVoiceMaterial 开始, ext=', ext, 'fileName=', file.name, 'size=', file.size);
|
||||
|
||||
if (ext === '.mp4') {
|
||||
// MP4 需要先本地提取音频
|
||||
tempVideoPath = await join('temp', `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.mp4`);
|
||||
console.log('[VoiceStore] tempVideoPath=', tempVideoPath);
|
||||
|
||||
// 1a. 将 File 写入本地临时文件(使用 BaseDirectory.AppLocalData)
|
||||
console.log('[VoiceStore] 开始 writeFile...');
|
||||
await mkdir('temp', { baseDir: BaseDirectory.AppLocalData, recursive: true });
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
await writeFile(tempVideoPath, uint8Array, { baseDir: BaseDirectory.AppLocalData });
|
||||
console.log('[VoiceStore] writeFile 完成');
|
||||
|
||||
// 1b. 调用 FFmpeg 提取音频(Rust 返回绝对路径)
|
||||
tempMp3Path = await voiceApi.extractAudioFromVideo(
|
||||
await join(await appLocalDataDir(), tempVideoPath)
|
||||
);
|
||||
const videoAbsPath = await join(await appLocalDataDir(), tempVideoPath);
|
||||
console.log('[VoiceStore] 开始 extractAudioFromVideo, path=', videoAbsPath);
|
||||
tempMp3Path = await voiceApi.extractAudioFromVideo(videoAbsPath);
|
||||
console.log('[VoiceStore] extractAudioFromVideo 完成, mp3Path=', tempMp3Path);
|
||||
|
||||
// 1c. 上传提取的 MP3
|
||||
console.log('[VoiceStore] 开始 uploadLocalAudioFile...');
|
||||
sourceUrl = await voiceApi.uploadLocalAudioFile(tempMp3Path);
|
||||
console.log('[VoiceStore] uploadLocalAudioFile 完成, url=', sourceUrl);
|
||||
} else {
|
||||
// 音频文件直接上传
|
||||
console.log('[VoiceStore] 直接上传音频文件...');
|
||||
sourceUrl = await voiceApi.uploadAudio(file);
|
||||
console.log('[VoiceStore] uploadAudio 完成, url=', sourceUrl);
|
||||
}
|
||||
|
||||
// 2. 提交 Vidu 同步克隆
|
||||
console.log('[VoiceStore] 开始 submitCloneTask...');
|
||||
const cloneResult = await voiceApi.submitCloneTask({
|
||||
sourceAudioUrl: sourceUrl,
|
||||
voiceName: name,
|
||||
});
|
||||
console.log('[VoiceStore] submitCloneTask 完成, result=', cloneResult);
|
||||
|
||||
// 3. Vidu 同步返回,直接 ready
|
||||
const material: VoiceMaterial = {
|
||||
@@ -174,26 +186,36 @@ export const useVoiceStore = create<VoiceState & VoiceActions>()(
|
||||
status: 'ready',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
console.log('[VoiceStore] material=', material);
|
||||
|
||||
// 4. 保存到本地 JSON
|
||||
console.log('[VoiceStore] 开始 saveVoiceMaterial...');
|
||||
await voiceApi.saveVoiceMaterial(material);
|
||||
console.log('[VoiceStore] saveVoiceMaterial 完成');
|
||||
set(state => ({ voiceMaterials: [material, ...state.voiceMaterials] }));
|
||||
|
||||
console.log('[VoiceStore] addVoiceMaterial 成功返回');
|
||||
return material;
|
||||
} catch (err) {
|
||||
console.error('[VoiceStore] addVoiceMaterial 异常:', err, '类型:', typeof err, '是Error?', err instanceof Error, 'message:', (err as Error)?.message);
|
||||
throw err;
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
console.log('[VoiceStore] finally 清理, tempVideoPath=', tempVideoPath, 'tempMp3Path=', tempMp3Path);
|
||||
if (tempVideoPath) {
|
||||
try {
|
||||
await remove(tempVideoPath, { baseDir: BaseDirectory.AppLocalData });
|
||||
} catch {
|
||||
// 忽略清理错误
|
||||
console.log('[VoiceStore] 清理 tempVideoPath 成功');
|
||||
} catch (e) {
|
||||
console.error('[VoiceStore] 清理 tempVideoPath 失败:', e);
|
||||
}
|
||||
}
|
||||
if (tempMp3Path) {
|
||||
try {
|
||||
await remove(tempMp3Path);
|
||||
} catch {
|
||||
// 忽略清理错误
|
||||
await remove(tempMp3Path, { baseDir: BaseDirectory.AppLocalData });
|
||||
console.log('[VoiceStore] 清理 tempMp3Path 成功');
|
||||
} catch (e) {
|
||||
console.error('[VoiceStore] 清理 tempMp3Path 失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user