195 lines
8.3 KiB
C#
195 lines
8.3 KiB
C#
using CloudBuilder.AI.Entity;
|
||
using CloudBuilder.Core.DatabaseAccessor.Entity;
|
||
using CloudBuilder.Core.DependencyInjection.Task;
|
||
using CloudBuilder.Topshelf.Utility;
|
||
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:TtsTask book_id:B000008 voice:Sherpa2 actor:韩立 actor_voice:Sherpa1 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice
|
||
//task:TtsTask voice:Sherpa2 save_path:D:\\Net8\\FileServer\\Backup\\DmsFile\\voice\\20260528112739.mp3 text:二愣子睁大着双眼,直直望着茅草和烂泥糊成的黑屋顶,身上盖着的旧棉被,已呈深黄色,看不出原来的本来面目,还若有若无的散发着淡淡的霉味。
|
||
public class TtsTask : IScheduleTask
|
||
{
|
||
private readonly IApplicationService service;
|
||
|
||
public TtsTask(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"];
|
||
|
||
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId).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 = ChineseTtsVoice.Sherpa2.ToString(), actor = string.Empty, actor_voice = ChineseTtsVoice.Sherpa1.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"];
|
||
|
||
GenerateAndMergeChapterAudio(ents, actor, actor_voice, voice, save_path);
|
||
}
|
||
|
||
public void GenerateAndMergeChapterAudio(AiSentenceViewEntity[] ents, string actor, string actor_voice, string mainVoice, string save_path)
|
||
{
|
||
// 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);
|
||
|
||
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)
|
||
{
|
||
processedLines++;
|
||
|
||
string voice = ChineseTtsVoice.Sherpa2.ToString();
|
||
if (ent.DialogueIndc == YesNoPolicy.YES)
|
||
{
|
||
if (ent.PersonName == actor)
|
||
voice = actor_voice;
|
||
else
|
||
voice = AI.Policy.ChineseTtsVoiceRandom.GetRandom().ToString();
|
||
}
|
||
else
|
||
{
|
||
voice = mainVoice;
|
||
}
|
||
|
||
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);
|
||
|
||
// 生成音频
|
||
SaveAudioToFileHelper.AudioToFile(EnumHelper.ToEnum<ChineseTtsVoice>(voice), ent.Content, filePath);
|
||
|
||
tempMp3List.Add(filePath); // 加入合并列表
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|