This commit is contained in:
owenchen committed 2026-06-22 17:02:26 +08:00
1 parent 77a62b8d88
commit 2c6475b2d5
10 files changed
+341 -234

No files matched your search

+277
View File
@@ -0,0 +1,277 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Policy;
using CloudBuilder.AI.Utility;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Python;
using CloudBuilder.Topshelf.Utility;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.IdentityModel.Tokens;
using NAudio.Lame;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace CloudBuilder.Topshelf.Task.TTS
{
//task:SoVITSTtsTask book_id:B000011 chapter_id:3 voice:MIDDLE_AGED_FEMALE5 actor:韩立 actor_voice:YOUNG_MALE2 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice
public class SoVITSTtsTask : IScheduleTask
{
private readonly IApplicationService service;
public SoVITSTtsTask(IApplicationService service)
{
this.service = service;
}
public void Run(Dictionary<string, string> bodyDict)
{
ConsoleOutput.HideStdError();
IRepository<AiSentenceViewEntity> repository = service.GetRepository<IRepository<AiSentenceViewEntity>>();
string bookId = string.Empty;
if (bodyDict.ContainsKey("book_id"))
bookId = bodyDict["book_id"];
string chapterId = string.Empty;
if (bodyDict.ContainsKey("chapter_id"))
chapterId = bodyDict["chapter_id"];
string from_chapter_id = string.Empty;
if (bodyDict.ContainsKey("from_chapter_id"))
from_chapter_id = bodyDict["from_chapter_id"];
string to_chapter_id = string.Empty;
if (bodyDict.ContainsKey("to_chapter_id"))
to_chapter_id = bodyDict["to_chapter_id"];
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId
&& (string.IsNullOrEmpty(chapterId) || x.ChapterId == Convert.ToInt32(chapterId))
&& (string.IsNullOrEmpty(from_chapter_id) || x.ChapterId >= Convert.ToInt32(from_chapter_id))
&& (string.IsNullOrEmpty(to_chapter_id) || x.ChapterId <= Convert.ToInt32(to_chapter_id)
)).OrderBy(x => x.ChapterId).ThenBy(x => x.ParagraphId).ThenBy(x => x.SentenceIndex).ToArray();
if (ents == null || ents.Length == 0) return;
string save_path = string.Empty, text = string.Empty, voice = SoVITSTtsVoiceRandom.GetRandomYoungFemale().ToString(), actor = string.Empty, actor_voice = SoVITSTtsVoiceRandom.GetRandomYoungMale().ToString();
if (bodyDict.ContainsKey("actor"))
actor = bodyDict["actor"];
if (bodyDict.ContainsKey("voice"))
voice = bodyDict["voice"];
if (bodyDict.ContainsKey("actor_voice"))
actor_voice = bodyDict["actor_voice"];
if (bodyDict.ContainsKey("save_path"))
save_path = bodyDict["save_path"];
float speed = 1.0f;
if (bodyDict.ContainsKey("speed"))
speed = Convert.ToSingle(bodyDict["speed"]);
GenerateAndMergeChapterAudio(ents, speed, actor, actor_voice, voice, save_path);
}
public void GenerateAndMergeChapterAudio(AiSentenceViewEntity[] ents, float speed, string actor, string actor_voice, string mainVoice, string save_path)
{
IRepository<AiBookPersonEntity> repositoryAiBookPersonEntity = service.GetRepository<IRepository<AiBookPersonEntity>>();
AiBookPersonEntity[] bps = repositoryAiBookPersonEntity.DetachedEntities.Where(x => x.BookId == ents[0].BookId).ToArray();
Python39Command python39Command = new Python39Command(service);
SoVITS soVITS = new SoVITS(python39Command);
// 1. 按 BookId + ChapterId 分组(关键!)
var chapterGroups = ents
.Where(ent => ent.Content.Any(c => c >= 0x4E00 && c <= 0x9FFF)) // 只保留含中文的句子
.GroupBy(ent => new { ent.BookId, ent.ChapterId })
.ToList();
int totalLines = ents.Count();
int processedLines = 0; // 已处理行数计数器
UpdateProgress(processedLines, totalLines);
Dictionary<string, string> personMapping = new Dictionary<string, string>();
foreach (var chapter in chapterGroups)
{
string bookId = chapter.Key.BookId;
int chapterId = chapter.Key.ChapterId;
List<string> tempMp3List = new List<string>(); // 本章所有碎片音频
// 2. 生成本章所有句子音频
foreach (AiSentenceViewEntity ent in chapter)
{
try
{
processedLines++;
string voice = SoVITSTtsVoiceRandom.GetRandomYoungFemale().ToString();
if (ent.DialogueIndc == YesNoPolicy.YES)
{
string key = string.IsNullOrEmpty(ent.PersonName) ? "NULL" : ent.PersonName;
if (key == actor)
{
if (personMapping.ContainsKey(key))
voice = personMapping[key];
else
{
voice = actor_voice;
personMapping.Add(key, voice);
}
}
else
{
if (!string.IsNullOrEmpty(key) && personMapping.ContainsKey(key))
{
voice = personMapping[key];
}
else if (!string.IsNullOrEmpty(key) && bps != null && bps.Length > 0 && bps.Where(x => x.PersonName == key).Any())
{
voice = bps.Where(x => x.PersonName == key).FirstOrDefault().ActorName;
personMapping.Add(key, voice);
}
else
{
if (ent.Gender == YesNoPolicy.YES)
{
voice = SoVITSTtsVoiceRandom.GetRandomFemaleExclude(EnumHelper.ToEnum<SoVITSTtsVoice>(actor_voice)).ToString();
personMapping.Add(key, voice);
}
else if (ent.Gender == YesNoPolicy.NO)
{
voice = SoVITSTtsVoiceRandom.GetRandomFemale().ToString();
personMapping.Add(key, voice);
}
else
{
voice = SoVITSTtsVoiceRandom.GetRandom().ToString();
personMapping.Add(key, voice);
}
}
}
}
else
{
voice = mainVoice;
if (personMapping.ContainsKey("mainVoice"))
voice = personMapping["mainVoice"];
else if (bps != null && bps.Length > 0 && bps.Where(x => string.IsNullOrEmpty(x.PersonName)).Any())
{
voice = bps.Where(x => string.IsNullOrEmpty(x.PersonName)).FirstOrDefault().ActorName;
personMapping.Add("mainVoice", voice);
}
}
if (string.IsNullOrEmpty(voice) || string.IsNullOrEmpty(ent.Content))
continue;
// 碎片文件名:BookId_ChapterId_ParagraphId_SentenceId.mp3
string fileName = string.Format("{0}_{1}_{2}_{3}.mp3", ent.BookId, ent.ChapterId, ent.ParagraphId, ent.SentenceId);
string filePath = Path.Combine(save_path, fileName);
// 生成音频
soVITS.Generate(ent.Content, soVITS.Voices[EnumHelper.ToEnum<SoVITSTtsVoice>(voice)], filePath);
tempMp3List.Add(filePath); // 加入合并列表
}
catch (Exception ex)
{
Console.WriteLine($"❌ 失败:{ex.Message}");
}
UpdateProgress(processedLines, totalLines);
}
Thread.Sleep(100);
// 3. 合并为:BookId_ChapterId.mp3
string outputFileName = string.Format("{0}_{1}.mp3", bookId, chapterId);
string outputFilePath = Path.Combine(save_path, outputFileName);
MergeChapterMp3Files(tempMp3List, outputFilePath);
// 4. 可选:合并后删除碎片文件(节省空间)
foreach (var chunk in tempMp3List)
{
try { File.Delete(chunk); } catch { }
}
UpdateProgress(processedLines, totalLines);
}
}
/// <summary>
/// 最终修复:完全解决 No fmt chunk / 采样率不兼容 / 只有一截声音
/// </summary>
public void MergeChapterMp3Files(List<string> chunkFiles, string outputFilePath)
{
var validFiles = chunkFiles.Where(File.Exists).ToList();
if (validFiles.Count == 0) return;
try
{
// 目标格式:32000Hz 单声道 16bit
WaveFormat targetFormat = new WaveFormat(32000, 16, 1);
// 直接生成 MP3,不经过 WAV 文件,彻底避免 fmt 错误
using (var mp3Writer = new LameMP3FileWriter(outputFilePath, targetFormat, 128))
{
foreach (var file in validFiles)
{
try
{
// 释放文件占用
GC.Collect();
GC.WaitForPendingFinalizers();
// 用Windows系统解码器读取任何MP3
using (var reader = new MediaFoundationReader(file))
using (var resampler = new MediaFoundationResampler(reader, targetFormat))
{
byte[] buffer = new byte[8192];
int read;
while ((read = resampler.Read(buffer, 0, buffer.Length)) > 0)
{
mp3Writer.Write(buffer, 0, read);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"跳过文件:{file},错误:{ex.Message}");
}
}
}
//Console.WriteLine($"✅ 合并成功:{outputFilePath},大小:{new FileInfo(outputFilePath).Length} 字节");
}
catch (Exception ex)
{
//Console.WriteLine($"❌ 合并失败:{ex.Message}");
}
}
private static void UpdateProgress(int processed, int total)
{
double progressPercent = (double)processed / total * 100;
int progressBarLength = 50; // 进度条总长度
int filledLength = (int)(progressPercent / 100 * progressBarLength);
// 构建进度条(如:[██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░])
string progressBar = "[" + new string('█', filledLength) + new string('░', progressBarLength - filledLength) + "]";
string progressInfo = $"{progressBar} {progressPercent:F2}% | 已处理:{processed}/{total} ";
Console.Write($"\r{progressInfo}");
Console.Out.Flush();
}
}
}
+1
View File
@@ -1,4 +1,5 @@
using CloudBuilder.AI.Entity;
using CloudBuilder.AI.Policy;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.DependencyInjection.Task;
using CloudBuilder.Topshelf.Utility;