Public Access
Add past project meeting summary tools
PR and Push Build/Test / build-and-test (push) Failing after 10m56s
PR and Push Build/Test / build-and-test (push) Failing after 10m56s
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MeetingAssistant.Tests")]
|
||||
@@ -8,9 +8,10 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
|
||||
public const string DefaultInitialPrompt = """
|
||||
You are the Meeting Assistant summary agent.
|
||||
|
||||
Use the provided tools to read the meeting transcript, assistant context, user notes, glossary, and project files.
|
||||
Use the provided tools to read the meeting transcript, assistant context, user notes, glossary, project files, and scoped past project meeting summaries.
|
||||
All read tools can return the whole file or a clamped inclusive line range when from and to are supplied; use ranges for large inputs before asking for more lines.
|
||||
Then write the finished meeting summary as markdown by calling write_summary. The write_summary call requires an oneliner parameter: keep it very short, blurb-like, and concise. No line breaks. Complete sentences are not necessary; conciseness is more important.
|
||||
write_summary is only for the current meeting's summary file. Past project meeting summaries are read-only historical context; use list_past_project_meetings and read_past_project_meeting_summary to inspect them, and never try to mutate them.
|
||||
If the meeting note has no title, provide a concise title parameter to write_summary.
|
||||
Use read_meetingnote to inspect frontmatter such as title, attendees, projects, start_time, and end_time.
|
||||
Use read_context and write_context as your own meeting notebook. Its frontmatter may include agenda from the calendar appointment. Record useful internal notes, missing context, requests for future tools, suggested improvements, and relevant context discovered from other sources. write_context appends by default; use replace_file=true only when you are intentionally replacing the whole assistant context body. Keep user-facing summary content in the summary note.
|
||||
@@ -19,7 +20,7 @@ public sealed class MeetingSummaryInstructionBuilder : IMeetingSummaryInstructio
|
||||
Use override_speaker only when you are very certain that a transcript speaker label belongs to a named person, for example from user notes, OCR evidence with a matching timestamp, or very clear context cues. Provide the exact speaker label from the transcript and the replacement speaker name. If the replacement speaker already exists in the transcript, use merge=true only when you are certain both speaker labels are the same identity; otherwise do not merge them.
|
||||
Use delete_identity only when you are certain that an existing speaker identity was wrongfully matched. Provide the exact identity name currently used in the transcript.
|
||||
After writing the summary, update existing project files when the meeting produced durable project knowledge, decisions, next steps, or context.
|
||||
Use list_projects first to see which projects are bound to this meeting. Use search and read_projectfile before changing existing project files.
|
||||
Use list_projects first to see which projects are bound to this meeting. Use search, list_past_project_meetings, read_past_project_meeting_summary, and read_projectfile before changing existing project files. search includes both project files and past meeting summaries for the requested current-meeting project scope.
|
||||
The summary note should contain concise sections for summary, decisions, open questions, and next steps.
|
||||
If the assistant context contains cropped screenshot markdown links, include only the most relevant cropped screenshots in the summary by markdown-linking them near the related summary text. Do not include every cropped screenshot by default, and do not link uncropped screenshots unless no cropped version exists and the image is important.
|
||||
Keep the output grounded in the source material and explicitly say when a section has no known items.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Transcription;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace MeetingAssistant.Summary;
|
||||
@@ -311,6 +309,45 @@ public sealed class MeetingSummaryTools
|
||||
return ReadLines(await File.ReadAllTextAsync(filePath), @from, to);
|
||||
}
|
||||
|
||||
public async Task<string> ListPastProjectMeetings(
|
||||
string[]? projects = null,
|
||||
int page = 1,
|
||||
int page_size = 20)
|
||||
{
|
||||
var scope = await ResolveCurrentMeetingProjectScopeAsync(projects);
|
||||
if (scope.Refusal is not null)
|
||||
{
|
||||
return scope.Refusal;
|
||||
}
|
||||
|
||||
var summaries = await GetScopedPastSummariesAsync(scope.ProjectNames);
|
||||
var effectivePageSize = Math.Clamp(page_size, 1, 100);
|
||||
var effectivePage = Math.Max(1, page);
|
||||
var totalPages = Math.Max(1, (int)Math.Ceiling(summaries.Count / (double)effectivePageSize));
|
||||
var pageItems = summaries
|
||||
.Skip((effectivePage - 1) * effectivePageSize)
|
||||
.Take(effectivePageSize)
|
||||
.ToList();
|
||||
var lines = new List<string>
|
||||
{
|
||||
$"page {Math.Min(effectivePage, totalPages)}/{totalPages}, page_size {effectivePageSize}, total {summaries.Count}"
|
||||
};
|
||||
lines.AddRange(pageItems.Select(summary =>
|
||||
$"{summary.ToolPath} | {FormatSummaryStartTime(summary.StartTime)} | {summary.Title} | projects: {string.Join(", ", summary.Projects)}"));
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
public async Task<string> ReadPastProjectMeetingSummary(string path, int? @from = null, int? to = null)
|
||||
{
|
||||
var summary = await ResolvePastSummaryAsync(path);
|
||||
if (summary.Refusal is not null)
|
||||
{
|
||||
return summary.Refusal;
|
||||
}
|
||||
|
||||
return ReadLines(await File.ReadAllTextAsync(summary.Summary!.Path), @from, to);
|
||||
}
|
||||
|
||||
public async Task<string> WriteProjectFile(
|
||||
string project,
|
||||
string path,
|
||||
@@ -354,17 +391,15 @@ public sealed class MeetingSummaryTools
|
||||
return "";
|
||||
}
|
||||
|
||||
var selectedProjects = await GetSearchProjectsAsync(projects);
|
||||
if (selectedProjects.Count == 0)
|
||||
var scope = await ResolveCurrentMeetingProjectScopeAsync(projects);
|
||||
if (scope.Refusal is not null)
|
||||
{
|
||||
return "";
|
||||
return scope.Refusal;
|
||||
}
|
||||
|
||||
var projectsRoot = GetProjectsRoot();
|
||||
var result = await RunRipgrepAsync(projectsRoot, selectedProjects, keywords);
|
||||
return result is not null
|
||||
? FormatRipgrepJson(result, selectedProjects.Count == 1)
|
||||
: SearchWithRegexFallback(selectedProjects, keywords, selectedProjects.Count == 1);
|
||||
var selectedProjects = await GetExistingProjectsInScopeAsync(scope.ProjectNames);
|
||||
var summaries = await GetScopedPastSummariesAsync(scope.ProjectNames);
|
||||
return SearchSources(selectedProjects, summaries, keywords, selectedProjects.Count == 1);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadIfExistsAsync(string path, int? from = null, int? to = null)
|
||||
@@ -462,19 +497,64 @@ public sealed class MeetingSummaryTools
|
||||
: value;
|
||||
}
|
||||
|
||||
private async Task<List<BoundMeetingProject>> GetSearchProjectsAsync(string[]? projects)
|
||||
private async Task<ProjectScope> ResolveCurrentMeetingProjectScopeAsync(string[]? projects)
|
||||
{
|
||||
var boundProjects = await GetBoundProjectsAsync();
|
||||
var currentProjects = await ReadCurrentMeetingProjectNamesAsync();
|
||||
if (currentProjects.Count == 0)
|
||||
{
|
||||
return new ProjectScope(new HashSet<string>(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (projects is null || projects.Length == 0)
|
||||
{
|
||||
return boundProjects;
|
||||
return new ProjectScope(currentProjects.ToHashSet(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var requested = projects
|
||||
.Where(project => !string.IsNullOrWhiteSpace(project))
|
||||
.Select(project => project.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var outOfScope = requested
|
||||
.Where(project => !currentProjects.Contains(project))
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (outOfScope.Count > 0)
|
||||
{
|
||||
return new ProjectScope(
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
||||
$"Refused: project(s) not assigned to current meeting: {string.Join(", ", outOfScope)}.");
|
||||
}
|
||||
|
||||
return new ProjectScope(requested.ToHashSet(StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task<HashSet<string>> ReadCurrentMeetingProjectNamesAsync()
|
||||
{
|
||||
var note = await ReadMeetingNoteAsync();
|
||||
return note.Frontmatter.Projects
|
||||
.Where(project => !string.IsNullOrWhiteSpace(project))
|
||||
.Select(project => project.Trim())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return boundProjects
|
||||
.Where(project => requested.Contains(project.Name))
|
||||
}
|
||||
|
||||
private async Task<List<BoundMeetingProject>> GetExistingProjectsInScopeAsync(IReadOnlySet<string> projectNames)
|
||||
{
|
||||
if (projectNames.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var projectsRoot = GetProjectsRoot();
|
||||
if (!Directory.Exists(projectsRoot))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return Directory.EnumerateDirectories(projectsRoot)
|
||||
.Select(path => new BoundMeetingProject(Path.GetFileName(path), path))
|
||||
.Where(project => projectNames.Contains(project.Name))
|
||||
.OrderBy(project => project.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -681,74 +761,11 @@ public sealed class MeetingSummaryTools
|
||||
return projectResolver.ProjectsRoot;
|
||||
}
|
||||
|
||||
private static async Task<string?> RunRipgrepAsync(
|
||||
string projectsRoot,
|
||||
private static string SearchSources(
|
||||
IReadOnlyList<BoundMeetingProject> projects,
|
||||
string keywords)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "rg",
|
||||
WorkingDirectory = projectsRoot,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
startInfo.ArgumentList.Add("--json");
|
||||
startInfo.ArgumentList.Add("--line-number");
|
||||
startInfo.ArgumentList.Add("--color");
|
||||
startInfo.ArgumentList.Add("never");
|
||||
startInfo.ArgumentList.Add("--");
|
||||
startInfo.ArgumentList.Add(keywords);
|
||||
foreach (var project in projects)
|
||||
{
|
||||
startInfo.ArgumentList.Add(project.Name);
|
||||
}
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
return process.ExitCode is 0 or 1 ? output : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatRipgrepJson(string output, bool singleProject)
|
||||
{
|
||||
var matches = new List<string>();
|
||||
using var reader = new StringReader(output);
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
using var document = JsonDocument.Parse(line);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("type", out var type) ||
|
||||
type.GetString() != "match" ||
|
||||
!root.TryGetProperty("data", out var data))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var path = data.GetProperty("path").GetProperty("text").GetString() ?? "";
|
||||
var lineNumber = data.GetProperty("line_number").GetInt32();
|
||||
var text = data.GetProperty("lines").GetProperty("text").GetString() ?? "";
|
||||
matches.Add($"{FormatSearchPath(path, singleProject)}:{lineNumber} {text.TrimEnd('\r', '\n')}");
|
||||
}
|
||||
|
||||
return string.Join('\n', matches);
|
||||
}
|
||||
|
||||
private static string SearchWithRegexFallback(IReadOnlyList<BoundMeetingProject> projects, string keywords, bool singleProject)
|
||||
IReadOnlyList<PastMeetingSummary> summaries,
|
||||
string keywords,
|
||||
bool singleProject)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -770,6 +787,11 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var summary in summaries)
|
||||
{
|
||||
AddFileMatches(matches, regex, summary.Path, $"past-meetings/{summary.ToolPath}");
|
||||
}
|
||||
|
||||
return string.Join('\n', matches);
|
||||
}
|
||||
catch
|
||||
@@ -778,6 +800,22 @@ public sealed class MeetingSummaryTools
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddFileMatches(
|
||||
List<string> matches,
|
||||
System.Text.RegularExpressions.Regex regex,
|
||||
string file,
|
||||
string formattedPath)
|
||||
{
|
||||
var lines = File.ReadAllLines(file);
|
||||
for (var index = 0; index < lines.Length; index++)
|
||||
{
|
||||
if (regex.IsMatch(lines[index]))
|
||||
{
|
||||
matches.Add($"{ToToolPath(formattedPath)}:{index + 1} {lines[index]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSearchPath(string path, bool singleProject)
|
||||
{
|
||||
var formatted = ToToolPath(path);
|
||||
@@ -804,8 +842,136 @@ public sealed class MeetingSummaryTools
|
||||
.Replace(Path.AltDirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private async Task<List<PastMeetingSummary>> GetScopedPastSummariesAsync(IReadOnlySet<string> projectNames)
|
||||
{
|
||||
if (projectNames.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var summariesRoot = GetSummariesRoot();
|
||||
if (!Directory.Exists(summariesRoot))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var summaries = new List<PastMeetingSummary>();
|
||||
foreach (var file in Directory.EnumerateFiles(summariesRoot, "*.md", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var summary = await TryReadPastSummaryAsync(file, summariesRoot);
|
||||
if (summary is null ||
|
||||
IsSamePath(summary.Path, artifacts.SummaryPath) ||
|
||||
!summary.Projects.Any(projectNames.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
summaries.Add(summary);
|
||||
}
|
||||
|
||||
return summaries
|
||||
.OrderByDescending(summary => summary.StartTime)
|
||||
.ThenByDescending(summary => summary.ToolPath, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<PastMeetingSummaryResolution> ResolvePastSummaryAsync(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || Path.IsPathRooted(path))
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: summary path must be relative to the configured summaries folder.");
|
||||
}
|
||||
|
||||
var scope = await ResolveCurrentMeetingProjectScopeAsync(null);
|
||||
if (scope.Refusal is not null)
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused(scope.Refusal);
|
||||
}
|
||||
|
||||
var summariesRoot = GetSummariesRoot();
|
||||
var fullPath = Path.GetFullPath(Path.Combine(summariesRoot, path));
|
||||
if (!IsWithinDirectory(summariesRoot, fullPath) || !File.Exists(fullPath))
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: summary file does not exist under the configured summaries folder.");
|
||||
}
|
||||
|
||||
if (IsSamePath(fullPath, artifacts.SummaryPath))
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: read_past_project_meeting_summary cannot read the current meeting summary; use write_summary only for the current meeting summary.");
|
||||
}
|
||||
|
||||
var summary = await TryReadPastSummaryAsync(fullPath, summariesRoot);
|
||||
if (summary is null)
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: file is not a summary note with project frontmatter.");
|
||||
}
|
||||
|
||||
if (!summary.Projects.Any(scope.ProjectNames.Contains))
|
||||
{
|
||||
return PastMeetingSummaryResolution.Refused("Refused: summary file is not assigned to a project in the current meeting scope.");
|
||||
}
|
||||
|
||||
return new PastMeetingSummaryResolution(summary);
|
||||
}
|
||||
|
||||
private async Task<PastMeetingSummary?> TryReadPastSummaryAsync(string path, string summariesRoot)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(path);
|
||||
var document = MarkdownDocumentParser.SplitOptional(content);
|
||||
if (!document.HasFrontmatter)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var frontmatter = YamlDeserializer.Deserialize<SummaryFrontmatterYaml>(document.Frontmatter)
|
||||
?? new SummaryFrontmatterYaml();
|
||||
var projects = (frontmatter.Projects ?? [])
|
||||
.Where(project => !string.IsNullOrWhiteSpace(project))
|
||||
.Select(project => project.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (projects.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PastMeetingSummary(
|
||||
path,
|
||||
ToToolPath(Path.GetRelativePath(summariesRoot, path)),
|
||||
string.IsNullOrWhiteSpace(frontmatter.Title) ? Path.GetFileNameWithoutExtension(path) : frontmatter.Title.Trim(),
|
||||
ParseDateTime(frontmatter.StartTime),
|
||||
projects);
|
||||
}
|
||||
|
||||
private string GetSummariesRoot()
|
||||
{
|
||||
return VaultPath.Resolve(options.Vault, options.Vault.SummariesFolder);
|
||||
}
|
||||
|
||||
private static string FormatSummaryStartTime(DateTimeOffset? startTime)
|
||||
{
|
||||
return startTime?.ToString("O") ?? "";
|
||||
}
|
||||
|
||||
private sealed record ProjectFileTarget(BoundMeetingProject Project, string Path);
|
||||
|
||||
private sealed record ProjectScope(IReadOnlySet<string> ProjectNames, string? Refusal = null);
|
||||
|
||||
private sealed record PastMeetingSummary(
|
||||
string Path,
|
||||
string ToolPath,
|
||||
string Title,
|
||||
DateTimeOffset? StartTime,
|
||||
IReadOnlyList<string> Projects);
|
||||
|
||||
private sealed record PastMeetingSummaryResolution(PastMeetingSummary? Summary, string? Refusal = null)
|
||||
{
|
||||
public static PastMeetingSummaryResolution Refused(string message)
|
||||
{
|
||||
return new PastMeetingSummaryResolution(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record FileLineEditMode(
|
||||
FileLineEditKind Kind,
|
||||
int? From = null,
|
||||
@@ -872,10 +1038,30 @@ public sealed class MeetingSummaryTools
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SummaryFrontmatterYaml
|
||||
{
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "start_time")]
|
||||
public string? StartTime { get; set; }
|
||||
|
||||
[YamlDotNet.Serialization.YamlMember(Alias = "projects")]
|
||||
public List<string>? Projects { get; set; }
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseDateTime(string? value)
|
||||
{
|
||||
return DateTimeOffset.TryParse(value, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool IsSamePath(string first, string second)
|
||||
{
|
||||
return string.Equals(
|
||||
Path.GetFullPath(first),
|
||||
Path.GetFullPath(second),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,10 +188,18 @@ public sealed class OpenAiMeetingSummaryAgentPipeline : IMeetingSummaryPipeline
|
||||
tools.WriteProjectFile,
|
||||
"write_projectfile",
|
||||
"Write a file inside an existing project folder. With no line arguments, append or create the file. Set replace_file=true only when intentionally replacing the whole file. With from and to, replace that inclusive 1-based line range. With insert, insert content at that 1-based line position."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.ListPastProjectMeetings,
|
||||
"list_past_project_meetings",
|
||||
"List paginated past meeting summary notes from the configured summaries folder for projects assigned to the current meeting. Optional projects must be assigned to the current meeting."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.ReadPastProjectMeetingSummary,
|
||||
"read_past_project_meeting_summary",
|
||||
"Read a past project meeting summary note as read-only historical context. The path is relative to the configured summaries folder. With from and to, read that clamped inclusive 1-based line range. Cannot read or write the current meeting summary."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.Search,
|
||||
"search",
|
||||
"Run ripgrep syntax keywords against the requested bound projects, or all projects bound to the meeting note.")
|
||||
"Search project files and past meeting summaries for the requested current-meeting project scope, or all projects assigned to the current meeting when projects is omitted.")
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -27,26 +27,7 @@ public sealed class ProcessCommandRunner : ICommandRunner
|
||||
CancellationToken cancellationToken,
|
||||
IReadOnlyDictionary<string, string>? environment = null)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
if (environment is not null)
|
||||
{
|
||||
foreach (var (key, value) in environment)
|
||||
{
|
||||
startInfo.Environment[key] = value;
|
||||
}
|
||||
}
|
||||
var startInfo = CreateStartInfo(fileName, arguments, environment);
|
||||
|
||||
logger.LogInformation("Running command {Command} {Arguments}", fileName, string.Join(' ', arguments));
|
||||
using var process = Process.Start(startInfo)
|
||||
@@ -82,6 +63,37 @@ public sealed class ProcessCommandRunner : ICommandRunner
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static ProcessStartInfo CreateStartInfo(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
IReadOnlyDictionary<string, string>? environment = null)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
if (environment is not null)
|
||||
{
|
||||
foreach (var (key, value) in environment)
|
||||
{
|
||||
startInfo.Environment[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadLinesAsync(
|
||||
StreamReader reader,
|
||||
Action<string> log,
|
||||
|
||||
@@ -66,6 +66,11 @@ public sealed class PyannoteDiarizationWarmupHostedService : IHostedService
|
||||
diarization.Image,
|
||||
diarization.Model);
|
||||
await finalizer.WarmUpAsync(diarization, cancellationToken);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Finished pyannote warm-up for image {Image} and model {Model}",
|
||||
diarization.Image,
|
||||
@@ -76,6 +81,10 @@ public sealed class PyannoteDiarizationWarmupHostedService : IHostedService
|
||||
logger.LogInformation("Pyannote warm-up was cancelled during application shutdown");
|
||||
throw;
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError(
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"MergeRecentIdentityAge": "14.00:00:00",
|
||||
"MatchTimeout": "00:03:00",
|
||||
"PyannoteValidation": {
|
||||
"Enabled": false,
|
||||
"Enabled": true,
|
||||
"MinimumSingleSpeakerCoverage": 0.9,
|
||||
"MinimumMatchingKnownSnippetRatio": 0.5,
|
||||
"Diarization": {
|
||||
|
||||
Reference in New Issue
Block a user