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

ストーリー作成タブを追加 #37

Merged
merged 8 commits into from
Apr 30, 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
32 changes: 19 additions & 13 deletions Epub/KoeBook.Epub/Services/AiStoryAnalyzerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,29 @@ public partial class AiStoryAnalyzerService(ISplitBraceService 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(),
Sections = [
new Section("本編")
{
Elements = aiStory.Lines.SelectMany(s =>
s.SelectMany(p => _splitBraceService.SplitBrace(p.GetText())
.Zip(_splitBraceService.SplitBrace(p.GetScript()))
.Select(Element (p) => new Paragraph
{
Text = p.First,
ScriptLine = new(p.Second, "", "")
}))
.Append(new Paragraph()
{
Text = "",
ScriptLine = new("", "", "")
aiueo-1234 marked this conversation as resolved.
Show resolved Hide resolved
})
).ToList(),
}
]
}]
};
}
Expand Down
5 changes: 3 additions & 2 deletions Epub/KoeBook.Epub/Services/AnalyzerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties,
var line = ReplaceBaseTextWithRuby(p.Text);

return p.ScriptLine = new ScriptLine(line, "", "");
}).ToList();
}).Where(l => !string.IsNullOrEmpty(l.Text))
.ToArray();

// 800文字以上になったら1チャンクに分ける
var chunks = new List<string>();
Expand All @@ -81,7 +82,7 @@ public async ValueTask<BookScripts> AnalyzeAsync(BookProperties bookProperties,
if (chunk.Length > 0) chunks.Add(chunk.ToString());

// GPT4による話者、スタイル解析
var bookScripts = await _llmAnalyzerService.LlmAnalyzeScriptLinesAsync(bookProperties, scriptLines, chunks, cancellationToken);
var bookScripts = await _llmAnalyzerService.LlmAnalyzeScriptLinesAsync(bookProperties, [.. scriptLines], chunks, cancellationToken);

return bookScripts;
}
Expand Down
9 changes: 9 additions & 0 deletions KoeBook.Core/Contracts/Services/IStoryCreatorService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using KoeBook.Core.Models;

namespace KoeBook.Core.Contracts.Services;

public interface IStoryCreatorService
{
/// <returns>XML</returns>
public ValueTask<string> CreateStoryAsync(StoryGenre genre, string instruction, CancellationToken cancellationToken);
}
34 changes: 16 additions & 18 deletions KoeBook.Core/Models/AiStory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,38 @@
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)
public class AiStory
{
private AiStory() : this("", []) { }

public record Section(
[property: XmlArrayItem("Paragraph", IsNullable = false)] Paragraph[] Paragraphs)
{
private Section() : this([]) { }
}
[XmlElement("Title", typeof(string), IsNullable = false)]
public string Title { get; init; } = "";

[XmlArray("Content", IsNullable = false)]
[XmlArrayItem("Section", IsNullable = false)]
[XmlArrayItem("Paragraph", IsNullable = false, NestingLevel = 1)]
public Line[][] Lines { get; init; } = [];

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

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

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

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

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

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

Expand All @@ -46,7 +44,7 @@ public record Ruby(
{
private Ruby() : this("", "") { }

public override string Text => Rb;
public override string Html => $"<ruby>{Rb}<rt>{Rt}</rt></ruby>";
public override string Script => Rt;
}
}
4 changes: 4 additions & 0 deletions KoeBook.Core/Models/StoryGenre.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace KoeBook.Core.Models;

public record class StoryGenre(string Genre, string Description);

4 changes: 4 additions & 0 deletions KoeBook/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public App()
services.AddTransient<ShellPage>();
services.AddTransient<ShellViewModel>();
services.AddTransient<EditDetailsViewModel>();
services.AddTransient<CreateStoryPage>();
services.AddTransient<CreateStoryViewModel>();

// Configuration
services.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)));
Expand All @@ -107,6 +109,8 @@ public App()
services.AddSingleton<ISoundGenerationSelectorService, SoundGenerationSelectorServiceMock>();
if (mockOptions.ISoundGenerationService.HasValue && mockOptions.ISoundGenerationService.Value)
services.AddSingleton<ISoundGenerationService, SoundGenerationServiceMock>();
if (mockOptions.IStoryCreaterService.HasValue && mockOptions.IStoryCreaterService.Value)
services.AddSingleton<IStoryCreatorService, StoryCreatorServiceMock>();
})
.Build();

Expand Down
9 changes: 9 additions & 0 deletions KoeBook/KoeBook.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<None Remove="Components\Dialog\DialogContentControl.xaml" />
<None Remove="Components\Dialog\SharedContentDialog.xaml" />
<None Remove="Components\StateProgressBar.xaml" />
<None Remove="Views\CreateStoryPage.xaml" />
<None Remove="Views\EditDetailsTab.xaml" />
</ItemGroup>

Expand Down Expand Up @@ -68,4 +69,12 @@
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>

<ItemGroup>
<CustomAdditionalCompileInputs Remove="Views\CreateStoryPage.xaml" />
</ItemGroup>

<ItemGroup>
<Resource Remove="Views\CreateStoryPage.xaml" />
</ItemGroup>
</Project>
2 changes: 2 additions & 0 deletions KoeBook/Models/MockOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ internal class MockOptions
public bool? IAnalyzerService { get; set; }

public bool? IEpubGenerateService { get; set; }

public bool? IStoryCreaterService { get; set; }
}
34 changes: 34 additions & 0 deletions KoeBook/Services/CoreMocks/StoryCreatorServiceMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using KoeBook.Core.Contracts.Services;
using KoeBook.Core.Models;

namespace KoeBook.Services.CoreMocks
{
public class StoryCreatorServiceMock : IStoryCreatorService
{
public ValueTask<string> CreateStoryAsync(StoryGenre genre, string instruction, CancellationToken cancellationToken)
{
return ValueTask.FromResult("""
<?xml version="1.0" encoding="UTF-8"?>
<Book>
<Title>境界線の向こう側</Title>
<Content>
<Section>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
</Section>
<Section>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
<Paragraph><Text>高校2年の夏、バレー部のエースで</Text><Ruby><Rb>端正</Rb><Rt>はんせい</Rt></Ruby><Text>な顔立ちの山田祐樹は、バスケ部のキャプテンで</Text><Ruby><Rb>凛</Rb><Rt>りん</Rt></Ruby><Text>とした佇まいの田中麻衣に密かに想いを寄せていた。しかし、両者の部活</Text><Ruby><Rb>仲間</Rb><Rt>なかま</Rt></Ruby><Text>たちの目を</Text><Ruby><Rb>憚</Rb><Rt>はばか</Rt></Ruby><Text>り、互いに素振りも見せずにいた。</Text></Paragraph>
</Section>
</Content>
</Book>
""");
}
}
}
79 changes: 79 additions & 0 deletions KoeBook/ViewModels/CreateStoryViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections.Immutable;
using System.Xml.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using KoeBook.Contracts.Services;
using KoeBook.Core.Contracts.Services;
using KoeBook.Core.Models;
using KoeBook.Models;

namespace KoeBook.ViewModels;

public sealed partial class CreateStoryViewModel : ObservableObject
{
private readonly IGenerationTaskService _generationTaskService;
private readonly IDialogService _dialogService;
private readonly IStoryCreatorService _storyCreatorService;

public ImmutableArray<StoryGenre> Genres { get; } = [
new("青春小説", "学校生活、友情、恋愛など、若者の成長物語"),
new("ミステリー・サスペンス", "謎解きや犯罪、真相究明などのスリリングな物語"),
new("SF", "未来、科学技術、宇宙などを題材にした物語"),
new("ホラー", "恐怖や怪奇現象を扱った、読者の恐怖心をくすぐる物語"),
new("ロマンス", "恋愛や結婚、人間関係などを扱った、胸キュンな物語"),
new("コメディ", "ユーモアやギャグ、風刺などを交えた、読者を笑わせる物語"),
new("歴史小説", "過去の出来事や人物を題材にした、歴史の背景が感じられる物語"),
new("ノンフィクション・エッセイ", "実際の経験や知識、考えを綴った、リアルな物語"),
new("詩集", "感情や思考、風景などを言葉で表現した、韻文形式の作品集"),
];

[ObservableProperty]
private StoryGenre _selectedGenre;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(CreateStoryCommand))]
private string _instruction = "";

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartGenerateTaskCommand))]
[NotifyPropertyChangedFor(nameof(AiStoryTitle))]
private AiStory? _aiStory;

public string AiStoryTitle => AiStory?.Title ?? "";

public CreateStoryViewModel(IGenerationTaskService generationTaskService, IDialogService dialogService, IStoryCreatorService storyCreatorService)
{
_selectedGenre = Genres[0];
_generationTaskService = generationTaskService;
_dialogService = dialogService;
_storyCreatorService = storyCreatorService;
}

public bool CanCreateStory => !string.IsNullOrWhiteSpace(Instruction);

[RelayCommand(CanExecute = nameof(CanCreateStory))]
private async Task OnCreateStoryAsync(CancellationToken cancellationToken)
{
using var sr = new StringReader(await _storyCreatorService.CreateStoryAsync(SelectedGenre, Instruction, cancellationToken));
var serializer = new XmlSerializer(typeof(AiStory));
try
{
AiStory = (AiStory?)serializer.Deserialize(sr);
}
catch (InvalidOperationException)
{
await _dialogService.ShowAsync("生成失敗", "AIによるコードの生成に失敗しました", "OK", cancellationToken);
}
}

public bool CanStartGenerate => AiStory is not null;

[RelayCommand(CanExecute = nameof(CanStartGenerate))]
private void OnStartGenerateTask()
{
var aiStory = AiStory!;
AiStory = null;
_generationTaskService.Register(new GenerationTask(Guid.NewGuid(), aiStory, true));
}
}

Loading
Loading