Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1-4 生成タスク周りをリファクタリング #31

Merged
merged 5 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Epub/KoeBook.Epub/Models/Paragraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public sealed class Paragraph : Element
{
public ScriptLine? ScriptLine { get; set; }
public Audio? Audio => ScriptLine?.Audio;
public string? Text { get; set; }
public string Text { get; set; } = "";
}
78 changes: 21 additions & 57 deletions Epub/KoeBook.Epub/Services/AnalyzerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ public partial class AnalyzerService(IScraperSelectorService scrapingService, IE
private readonly IScraperSelectorService _scrapingService = scrapingService;
private readonly IEpubDocumentStoreService _epubDocumentStoreService = epubDocumentStoreService;
private readonly ILlmAnalyzerService _llmAnalyzerService = llmAnalyzerService;
private Dictionary<string, string> _rubyReplacements = new Dictionary<string, string>();

public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, string coverFilePath, CancellationToken cancellationToken)
public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, CancellationToken cancellationToken)
{
coverFilePath = Path.Combine(tempDirectory, "Cover.png");
Directory.CreateDirectory(tempDirectory);
var coverFilePath = Path.Combine(tempDirectory, "Cover.png");
using var fs = File.Create(coverFilePath);
await fs.WriteAsync(CoverFile.ToArray(), cancellationToken);
await fs.FlushAsync(cancellationToken);
Expand All @@ -27,43 +27,27 @@ public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties,
{
document = await _scrapingService.ScrapingAsync(bookProperties.Source, coverFilePath, tempDirectory, bookProperties.Id, cancellationToken);
}
catch (EbookException)
{
throw;
}
catch (Exception ex)
{
EbookException.Throw(ExceptionType.WebScrapingFailed, "", ex);
EbookException.Throw(ExceptionType.WebScrapingFailed, innerException: ex);
return default;
}
_epubDocumentStoreService.Register(document, cancellationToken);

var scriptLines = new List<ScriptLine>();
foreach (var chapter in document.Chapters)
{
foreach (var section in chapter.Sections)
var scriptLines = document.Chapters.SelectMany(c => c.Sections)
.SelectMany(s => s.Elements)
.OfType<Paragraph>()
.Select(p =>
{
foreach (var element in section.Elements)
{
if (element is Paragraph paragraph)
{
var line = paragraph.Text;
// rubyタグがあればルビのdictionaryに登録
var rubyDict = ExtractRuby(line);
// ルビを置換
var line = ReplaceBaseTextWithRuby(p.Text);

foreach (var ruby in rubyDict)
{
if (!_rubyReplacements.ContainsKey(ruby.Key))
{
_rubyReplacements.Add(ruby.Key, ruby.Value);
}
}
// ルビを置換
line = ReplaceBaseTextWithRuby(line, rubyDict);

var scriptLine = new ScriptLine(line, "", "");
paragraph.ScriptLine = scriptLine;
scriptLines.Add(scriptLine);
}
}
}
}
return p.ScriptLine = new ScriptLine(line, "", "");
}).ToList();

// 800文字以上になったら1チャンクに分ける
var chunks = new List<string>();
Expand All @@ -85,32 +69,12 @@ public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties,
return bookScripts;
}

private static Dictionary<string, string> ExtractRuby(string text)
{
var rubyDict = new Dictionary<string, string>();
var rubyRegex = new Regex("<ruby><rb>(.*?)</rb><rp>(</rp><rt>(.*?)</rt><rp>)</rp></ruby>");

foreach (Match match in rubyRegex.Matches(text))
{
if (!rubyDict.ContainsKey(match.Groups[1].Value))
{
rubyDict.Add(match.Groups[1].Value, match.Groups[2].Value);
}
}

return rubyDict;
}

private static string ReplaceBaseTextWithRuby(string text, Dictionary<string, string> rubyDict)
private static string ReplaceBaseTextWithRuby(string text)
{
// 元のテキストからルビタグをすべてルビテキストに置き換える
var resultText = text;
foreach (var pair in rubyDict)
{
var rubyTag = $"<ruby><rb>{pair.Key}</rb><rp>(</rp><rt>{pair.Value}</rt><rp>)</rp></ruby>";
resultText = resultText.Replace(rubyTag, pair.Value);
}

return resultText;
return RubyRegex().Replace(text, m => m.Groups[2].Value);
}

[GeneratedRegex(@"<ruby>\s*<rb>(.*?)</rb>\s*<rp>\s*[(《\(]\s*</rp>\s*<rt>(.*?)</rt>\s*<rp>\s*[)》\)]\s*</rp>\s*</ruby>", RegexOptions.Multiline)]
private static partial Regex RubyRegex();
}
4 changes: 1 addition & 3 deletions Epub/KoeBook.Epub/Services/EpubGenerateService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using KoeBook.Core;
using KoeBook.Core.Contracts.Services;
using KoeBook.Core.Models;
using KoeBook.Epub;
using KoeBook.Epub.Contracts.Services;
using KoeBook.Epub.Models;

Expand All @@ -17,8 +16,7 @@ public async ValueTask<string> GenerateEpubAsync(BookScripts bookScripts, string
{
cancellationToken.ThrowIfCancellationRequested();

var document = _documentStoreService.Documents.Where(doc => doc.Id == bookScripts.BookProperties.Id).FirstOrDefault()
?? throw new InvalidOperationException($"The epub document ({bookScripts.BookProperties.Id}) can't be found.");
var document = _documentStoreService.Documents.Single(d => d.Id == bookScripts.BookProperties.Id);

foreach (var scriptLine in bookScripts.ScriptLines)
{
Expand Down
2 changes: 1 addition & 1 deletion KoeBook.Core/Contracts/Services/IAnalyzerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public interface IAnalyzerService
/// 本の情報の取得・解析を行います
/// </summary>
/// <returns>編集前の読み上げテキスト</returns>
ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, string coverFilePath, CancellationToken cancellationToken);
ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, CancellationToken cancellationToken);
}
2 changes: 2 additions & 0 deletions KoeBook.Core/Contracts/Services/IDisplayStateChangeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public interface IDisplayStateChangeService
/// </summary>
void UpdateState(BookProperties bookProperties, GenerationState state);

void UpdateTitle(BookProperties bookProperties, string title);

/// <summary>
/// プログレスバーを更新します
/// </summary>
Expand Down
16 changes: 15 additions & 1 deletion KoeBook.Core/Helpers/IDisplayStateChangeEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,23 @@ public class DisplayStateChanging(IDisplayStateChangeService displayStateChangeS

private readonly int _maximum = maximum;

private int _progress;

public void UpdateProgress(int progress)
{
_displayStateChangeService.UpdateProgress(_bookProperties, progress, _maximum);
_displayStateChangeService.UpdateProgress(_bookProperties, _progress = progress, _maximum);
}

public void IncrementProgress()
{
_progress++;
_displayStateChangeService.UpdateProgress(_bookProperties, _progress, _maximum);
}

public void Finish()
{
_progress = _maximum;
_displayStateChangeService.UpdateProgress(_bookProperties, _progress, _maximum);
}
}
}
33 changes: 33 additions & 0 deletions KoeBook.Test/Epub/AnalyzerServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Runtime.CompilerServices;
using KoeBook.Epub.Services;

namespace KoeBook.Test.Epub;

public class AnalyzerServiceTest
{
[Theory]
[InlineData("aa", "aa")]
[InlineData("<ruby><rb>漢字</rb><rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>", "かんじ")]
[InlineData("ああ<ruby><rb>漢字</rb><rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>あああ", "ああかんじあああ")]
[InlineData("""
ああ<ruby><rb>漢字</rb><rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>あああ
ああ<ruby><rb>漢字</rb><rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>あああ
ああ<ruby><rb>漢字1</rb><rp>(</rp><rt>かんじ1</rt><rp>)</rp></ruby>あああ
""", "ああかんじあああ\nああかんじあああ\nああかんじ1あああ")]
[InlineData("<ruby> <rb>佐久平</rb> <rp>\n《 </rp> <rt>さくだいら</rt> <rp>》</rp> </ruby> <ruby><rb>啓介</rb><rp>《</rp><rt>けいすけ</rt><rp>》</rp></ruby>",
"さくだいら けいすけ")]
[InlineData("<ruby><rb>漢字</rb>\n<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>", "かんじ")]
[InlineData("ああ<ruby><rb>漢字</rb><rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>あああ<ruby><rb>漢字</rb><rp>(</rp><rt>カンジ</rt><rp>)</rp></ruby>", "ああかんじあああカンジ")]
public void ReplaceBaseTextWithRuby(string input, string expected)
aiueo-1234 marked this conversation as resolved.
Show resolved Hide resolved
{
var result = AnalyzerServiceProxy.ReplaceBaseTextWithRuby(null, input);

Assert.Equal(expected, result);
}
}

file static class AnalyzerServiceProxy
{
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod)]
public static extern string ReplaceBaseTextWithRuby(AnalyzerService? _, string text);
}
4 changes: 2 additions & 2 deletions KoeBook.Test/Epub/EpubDocumentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void EnsureParagraph()
var element = Assert.Single(section.Elements);
var paragraph = Assert.IsType<Paragraph>(element);
Assert.Null(paragraph.Audio);
Assert.Null(paragraph.Text);
Assert.Empty(paragraph.Text);
Assert.Null(paragraph.ClassName);

// 空でないときは無視
Expand Down Expand Up @@ -129,7 +129,7 @@ public void EnsureParagraph()
element = Assert.Single(document.Chapters[0].Sections[1].Elements);
paragraph = Assert.IsType<Paragraph>(element);
Assert.Null(paragraph.Audio);
Assert.Null(paragraph.Text);
Assert.Empty(paragraph.Text);
Assert.Null(paragraph.ClassName);

// インデックスは正しく指定する必要がある
Expand Down
4 changes: 1 addition & 3 deletions KoeBook/Services/CoreMocks/AnalyzerServiceMock.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using KoeBook.Core.Contracts.Services;
using KoeBook.Core.Helpers;
using KoeBook.Core.Models;
using KoeBook.Epub;
using KoeBook.Epub.Models;
using static KoeBook.Core.Helpers.IDisplayStateChangeEx;

namespace KoeBook.Services.CoreMocks;
Expand All @@ -11,7 +9,7 @@ public class AnalyzerServiceMock(IDisplayStateChangeService stateService) : IAna
{
private readonly IDisplayStateChangeService _stateService = stateService;

public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, string coverFilePath, CancellationToken cancellationToken)
public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, CancellationToken cancellationToken)
{
DisplayStateChanging stateChanging;
if (bookProperties.SourceType == SourceType.Url)
Expand Down
9 changes: 9 additions & 0 deletions KoeBook/Services/DisplayStateChangeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ public void UpdateState(BookProperties bookProperties, GenerationState state)
taskService.GetProcessingTask(bookProperties.Id).State = state;
});
}

public void UpdateTitle(BookProperties bookProperties, string title)
{
var taskService = _taskService; // thisをキャプチャしないようにする
_ = App.MainWindow.DispatcherQueue.TryEnqueue(() =>
{
taskService.GetProcessingTask(bookProperties.Id).Title = title;
});
}
}
70 changes: 33 additions & 37 deletions KoeBook/Services/GenerationTaskRunnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,50 +48,45 @@ private async void TasksChanged(GenerationTask task, ChangedEvents changedEvents

private async ValueTask RunAsync(GenerationTask task)
{
try
{
var scripts = await _analyzerService.AnalyzeAsync(new(task.Id, task.Source, task.SourceType), _tempFolder, "", task.CancellationToken);
task.BookScripts = scripts;
task.State = GenerationState.Editting;
task.Progress = 0;
task.MaximumProgress = 0;
if (task.SkipEdit)
{
var resultPath = await _epubGenService.GenerateEpubAsync(scripts, _tempFolder, task.CancellationToken);
task.State = GenerationState.Completed;
task.Progress = 1;
task.MaximumProgress = 1;
var fileName = Path.GetFileName(resultPath);
File.Copy(resultPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "KoeBook", fileName), true);
}
}
catch (OperationCanceledException)
{
task.State = GenerationState.Failed;
}
catch (EbookException e)
{
task.State = GenerationState.Failed;
await _dialogService.ShowInfoAsync("生成失敗", e.ExceptionType.GetEnumMemberValue()!, "OK", default);
}
catch
{
task.State = GenerationState.Failed;
}
if (task.CancellationToken.IsCancellationRequested || task.State == GenerationState.Failed)
return;

await RunAsyncCore(task, true);
await RunAsyncCore(task, false);
}

public async void RunGenerateEpubAsync(GenerationTask task)
{
if (task.CancellationToken.IsCancellationRequested || task.State == GenerationState.Failed || task.BookScripts is null)
return;

await RunAsyncCore(task, false);
}

private async ValueTask RunAsyncCore(GenerationTask task, bool firstStep)
{
var tempDirectory = Path.Combine(_tempFolder, task.Id.ToString());
try
{
var resultPath = await _epubGenService.GenerateEpubAsync(task.BookScripts, _tempFolder, task.CancellationToken);
task.State = GenerationState.Completed;
task.Progress = 1;
task.MaximumProgress = 1;
var fileName = Path.GetFileName(resultPath);
File.Copy(resultPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "KoeBook", fileName), true);
if (firstStep)
{
var scripts = await _analyzerService.AnalyzeAsync(new(task.Id, task.Source, task.SourceType), tempDirectory, task.CancellationToken);
task.BookScripts = scripts;
task.State = GenerationState.Editting;
task.Progress = 0;
task.MaximumProgress = 0;
}
else if (task.BookScripts is not null)
{
var resultPath = await _epubGenService.GenerateEpubAsync(task.BookScripts, tempDirectory, task.CancellationToken);
task.State = GenerationState.Completed;
task.Progress = 1;
task.MaximumProgress = 1;
var fileName = Path.GetFileName(resultPath);
File.Move(resultPath, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "KoeBook", fileName), true);
}
else
throw new InvalidOperationException();
}
catch (OperationCanceledException)
{
Expand All @@ -102,9 +97,10 @@ public async void RunGenerateEpubAsync(GenerationTask task)
task.State = GenerationState.Failed;
await _dialogService.ShowInfoAsync("生成失敗", e.ExceptionType.GetEnumMemberValue()!, "OK", default);
}
catch
catch (Exception e)
{
task.State = GenerationState.Failed;
await _dialogService.ShowInfoAsync("生成失敗", $"不明なエラーが発生しました。\n{e.Message}", "OK", default);
}
}

Expand Down
Loading