CloudBuilder.Topshelf/Task/TTS/TtsTask.cs
owenchen dbfa9bd1e8 ow
2026-06-04 16:02:14 +08:00

222 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 chapter_id:3 speed:1 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"];
string chapterId = string.Empty;
if (bodyDict.ContainsKey("chapter_id"))
chapterId = bodyDict["chapter_id"];
AiSentenceViewEntity[] ents = repository.DetachedEntities.Where(x => x.BookId == bookId && (string.IsNullOrEmpty(chapterId) || x.ChapterId == Convert.ToInt32(chapterId))).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"];
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)
{
// 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)
{
processedLines++;
string voice = ChineseTtsVoice.Sherpa2.ToString();
if (ent.DialogueIndc == YesNoPolicy.YES)
{
if (ent.PersonName == actor)
{
if (personMapping.ContainsKey(ent.PersonName))
voice = personMapping[ent.PersonName];
else
{
voice = actor_voice;
personMapping.Add(ent.PersonName, voice);
}
}
else
{
if (!string.IsNullOrEmpty(ent.PersonName) && personMapping.ContainsKey(ent.PersonName))
voice = personMapping[ent.PersonName];
else
{
voice = CloudBuilder.AI.Policy.ChineseTtsVoiceRandom.GetRandom(80).ToString();
personMapping.Add(ent.PersonName, voice);
}
}
}
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), speed, 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();
}
}
}