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

xmlの小説の読み込み部分の作成 #38

Merged
merged 11 commits into from
Apr 29, 2024
34 changes: 34 additions & 0 deletions Epub/KoeBook.Epub/Services/AiStoryAnalyzerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using KoeBook.Epub.Contracts.Services;
using KoeBook.Epub.Models;
using KoeBook.Models;

namespace KoeBook.Epub.Services;

public partial class AiStoryAnalyzerService(ISplitBraceService splitBraceService)
{
private readonly ISplitBraceService _splitBraceService = splitBraceService;


public EpubDocument CreateEpubDocument(AiStory aiStory, Guid id)
{
int sectionNumber = 1;
return new EpubDocument(aiStory.Title, "AI", "", id)
{
Chapters = [new Chapter()
{
Sections = aiStory.Sections.Select(s => new Section($"第{sectionNumber++}章")
{
Elements = s.Paragraphs.SelectMany(p =>
_splitBraceService.SplitBrace(p.GetText())
.Zip(_splitBraceService.SplitBrace(p.GetScript()))
.Select(Element (p) => new Paragraph
{
Text = p.First,
ScriptLine = new(p.Second, "", "")
})
).ToList(),
}).ToList(),
}]
};
}
}
39 changes: 28 additions & 11 deletions Epub/KoeBook.Epub/Services/AnalyzerService.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
using System.Text;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using KoeBook.Core;
using KoeBook.Core.Contracts.Services;
using KoeBook.Core.Models;
using KoeBook.Epub.Contracts.Services;
using KoeBook.Epub.Models;
using KoeBook.Models;

namespace KoeBook.Epub.Services;

public partial class AnalyzerService(IScraperSelectorService scrapingService, IEpubDocumentStoreService epubDocumentStoreService, ILlmAnalyzerService llmAnalyzerService) : IAnalyzerService
public partial class AnalyzerService(
IScraperSelectorService scrapingService,
IEpubDocumentStoreService epubDocumentStoreService,
ILlmAnalyzerService llmAnalyzerService,
AiStoryAnalyzerService aiStoryAnalyzerService) : IAnalyzerService
{
private readonly IScraperSelectorService _scrapingService = scrapingService;
private readonly IEpubDocumentStoreService _epubDocumentStoreService = epubDocumentStoreService;
private readonly ILlmAnalyzerService _llmAnalyzerService = llmAnalyzerService;
private readonly AiStoryAnalyzerService _aiStoryAnalyzerService = aiStoryAnalyzerService;

public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties, string tempDirectory, CancellationToken cancellationToken)
{
Expand All @@ -22,26 +29,36 @@ public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties,
await fs.WriteAsync(CoverFile.ToArray(), cancellationToken);
await fs.FlushAsync(cancellationToken);

EpubDocument? document;
var rubyReplaced = false;
EpubDocument document;
try
{
document = await _scrapingService.ScrapingAsync(bookProperties.Source, coverFilePath, tempDirectory, bookProperties.Id, cancellationToken);
}
catch (EbookException)
{
throw;
switch (bookProperties)
{
case { SourceType: SourceType.Url or SourceType.FilePath, Source: string uri }:
document = await _scrapingService.ScrapingAsync(uri, coverFilePath, tempDirectory, bookProperties.Id, cancellationToken);
break;
case { SourceType: SourceType.AiStory, Source: AiStory aiStory }:
document = _aiStoryAnalyzerService.CreateEpubDocument(aiStory, bookProperties.Id);
rubyReplaced = true;
break;
default:
throw new UnreachableException($"SourceType: {bookProperties.SourceType}, Source: {bookProperties.Source}");
}
}
catch (EbookException) { throw; }
catch (Exception ex)
{
EbookException.Throw(ExceptionType.WebScrapingFailed, innerException: ex);
return default;
throw new EbookException(ExceptionType.WebScrapingFailed, innerException: ex);
}
_epubDocumentStoreService.Register(document, cancellationToken);

var scriptLines = document.Chapters.SelectMany(c => c.Sections)
.SelectMany(s => s.Elements)
.OfType<Paragraph>()
.Select(p =>
.Select<Paragraph, ScriptLine>(rubyReplaced
? p => p.ScriptLine!
: p =>
{
// ルビを置換
var line = ReplaceBaseTextWithRuby(p.Text);
Expand Down
3 changes: 3 additions & 0 deletions KoeBook.Core/EbookException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ public enum ExceptionType
/// </summary>
[EnumMember(Value = "無効なURLです")]
InvalidUrl,

[EnumMember(Value = "不正なXMLです")]
InvalidXml,
}
52 changes: 52 additions & 0 deletions KoeBook.Core/Models/AiStory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Xml.Serialization;

namespace KoeBook.Models;

[XmlRoot("Book")]
public record AiStory(
[property: XmlElement("Title", typeof(string), IsNullable = false)] string Title,
[property: XmlArray("Content", IsNullable = false), XmlArrayItem("Section", IsNullable = false)] AiStory.Section[] Sections)
{
private AiStory() : this("", []) { }

public record Section(
[property: XmlArrayItem("Paragraph", IsNullable = false)] Paragraph[] Paragraphs)
{
private Section() : this([]) { }
}


public record Paragraph(
[property: XmlElement("Text", typeof(TextElement), IsNullable = false), XmlElement("Ruby", typeof(Ruby), IsNullable = false)] InlineElement[] Inlines)
{
private Paragraph() : this([]) { }

public string GetText() => string.Concat(Inlines.Select(e => e.Text));

public string GetScript() => string.Concat(Inlines.Select(e => e.Script));
}

public abstract record class InlineElement
{
public abstract string Text { get; }
public abstract string Script { get; }
}

public record TextElement([property: XmlText] string InnerText) : InlineElement
{
private TextElement() : this("") { }

public override string Text => InnerText;
public override string Script => InnerText;
}

public record Ruby(
[property: XmlElement("Rb", IsNullable = false)] string Rb,
[property: XmlElement("Rt", IsNullable = false)] string Rt) : InlineElement
{
private Ruby() : this("", "") { }

public override string Text => Rb;
public override string Script => Rt;
}
}
31 changes: 26 additions & 5 deletions KoeBook.Core/Models/BookProperties.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
namespace KoeBook.Core.Models;
using KoeBook.Models;

namespace KoeBook.Core.Models;

/// <summary>
/// 読み上げる本の情報
/// </summary>
public class BookProperties(Guid id, string source, SourceType sourceType)
public class BookProperties
{
public Guid Id { get; } = id;
public BookProperties(Guid id, string source, SourceType sourceType)
{
if (sourceType != SourceType.FilePath && sourceType != SourceType.Url)
throw new ArgumentException($"{nameof(sourceType)}は{nameof(SourceType.FilePath)}か{nameof(SourceType.Url)}である必要があります。");
Id = id;
Source = source;
SourceType = sourceType;
}

public BookProperties(Guid id, AiStory aiStory)
{
Id = id;
Source = aiStory;
SourceType = SourceType.AiStory;
aiueo-1234 marked this conversation as resolved.
Show resolved Hide resolved
}

public Guid Id { get; }

public string Source { get; } = source;
/// <summary>
/// UriまたはAiStory
/// </summary>
public object Source { get; }

public SourceType SourceType { get; } = sourceType;
public SourceType SourceType { get; }
}
3 changes: 3 additions & 0 deletions KoeBook.Core/Models/SourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ public enum SourceType

[EnumMember(Value = "ローカルファイル")]
FilePath,

[EnumMember(Value = "AI生成")]
AiStory,
}
48 changes: 39 additions & 9 deletions KoeBook/Models/GenerationTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,57 @@

namespace KoeBook.Models;

public partial class GenerationTask(Guid id, string source, SourceType sourceType, bool skipEdit) : ObservableObject
public partial class GenerationTask : ObservableObject
{
public Guid Id { get; } = id;
public GenerationTask(Guid id, string source, SourceType sourceType, bool skipEdit)
{
if (sourceType != SourceType.FilePath && sourceType != SourceType.Url)
throw new ArgumentException($"{nameof(sourceType)}は{nameof(SourceType.FilePath)}か{nameof(SourceType.Url)}である必要があります。");
Id = id;
_rawSource = source;
SourceType = sourceType;
_skipEdit = skipEdit;
_title = sourceType == SourceType.FilePath ? Path.GetFileName(source) : source;
aiueo-1234 marked this conversation as resolved.
Show resolved Hide resolved
}

public GenerationTask(Guid id, AiStory aiStory, bool skipEdit)
{
Id = id;
_rawSource = aiStory;
SourceType = SourceType.AiStory;
_skipEdit = skipEdit;
_title = aiStory.Title;
}

public BookProperties ToBookProperties()
{
return SourceType == SourceType.AiStory
? new BookProperties(Id, (AiStory)_rawSource)
: new BookProperties(Id, Source, SourceType);
}

public Guid Id { get; }

public CancellationTokenSource CancellationTokenSource { get; } = new();

public CancellationToken CancellationToken => CancellationTokenSource.Token;

public string Source { get; } = source;
public string Source => _rawSource is string uri ? uri : "AI生成";

private readonly object _rawSource;

public SourceType SourceType { get; } = sourceType;
public SourceType SourceType { get; }

public string SourceDescription => SourceType switch
{
SourceType.Url => "URL",
SourceType.FilePath => "ファイルパス",
SourceType.AiStory => "AI生成",
_ => string.Empty,
};

[ObservableProperty]
private string _title = sourceType == SourceType.FilePath ? Path.GetFileName(source) : source;
private string _title;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ProgressText))]
Expand All @@ -36,7 +66,7 @@ public partial class GenerationTask(Guid id, string source, SourceType sourceTyp

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StateText))]
[NotifyPropertyChangedFor(nameof(SkipEditChangable))]
[NotifyPropertyChangedFor(nameof(SkipEditChangeable))]
[NotifyPropertyChangedFor(nameof(Editable))]
private GenerationState _state;

Expand All @@ -49,17 +79,17 @@ public bool SkipEdit
get => _skipEdit;
set
{
if (_skipEdit != value && SkipEditChangable)
if (_skipEdit != value && SkipEditChangeable)
{
OnPropertyChanging(nameof(SkipEdit));
_skipEdit = value;
OnPropertyChanged(nameof(SkipEdit));
}
}
}
private bool _skipEdit = skipEdit;
private bool _skipEdit;

public bool SkipEditChangable => State < GenerationState.Editting;
public bool SkipEditChangeable => State < GenerationState.Editting;

public bool Editable => State == GenerationState.Editting;

Expand Down
2 changes: 1 addition & 1 deletion KoeBook/Services/GenerationTaskRunnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private async ValueTask RunAsyncCore(GenerationTask task, bool firstStep)
{
if (firstStep)
{
var scripts = await _analyzerService.AnalyzeAsync(new(task.Id, task.Source, task.SourceType), tempDirectory, task.CancellationToken);
var scripts = await _analyzerService.AnalyzeAsync(task.ToBookProperties(), tempDirectory, task.CancellationToken);
task.BookScripts = scripts;
task.State = GenerationState.Editting;
task.Progress = 0;
Expand Down
3 changes: 2 additions & 1 deletion KoeBook/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public static IHostBuilder UseCoreStartup(this IHostBuilder builder)
.AddKeyedSingleton<IScrapingClientService, ScrapingClientService>(nameof(ScrapingNaroService))
.AddSingleton<IScraperSelectorService, ScraperSelectorService>()
.AddSingleton<IScrapingService, ScrapingAozoraService>()
.AddSingleton<IScrapingService, ScrapingNaroService>();
.AddSingleton<IScrapingService, ScrapingNaroService>()
.AddSingleton<AiStoryAnalyzerService>();
services.AddSingleton<IEpubCreateService, EpubCreateService>();
services.AddSingleton<ISplitBraceService, SplitBraceService>();
services.AddSingleton<IFileExtensionService, FileExtensionService>();
Expand Down
2 changes: 1 addition & 1 deletion KoeBook/Views/EditDetailsTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
Margin="{StaticResource XXSmallLeftTopRightBottomMargin}"
OffContent="編集する"
OnContent="編集しない"
IsEnabled="{x:Bind ViewModel.Task.SkipEditChangable, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.Task.SkipEditChangeable, Mode=OneWay}"
IsOn="{x:Bind ViewModel.Task.SkipEdit, Mode=TwoWay}"/>

<TextBlock
Expand Down
Loading