PPT 템플릿 품질 게이트와 manifest 추천 흐름을 고도화한다
- PptxSkill에 template master clone 진단 코드와 Template diagnostics 출력 라인을 추가해 color fallback 원인을 asset missing / clone failure로 구분한다. - PptQualityGatePolicy와 templates.manifest.json, PptxTemplateManifestCatalog를 확장해 manifest 메타데이터 기반 추천과 원인별 재루프 프롬프트를 연결한다. - PptQualityGatePolicyTests, PptxTemplateManifestCatalogTests, PptxSkillTemplatePackTests, PptxSkillTemplateDiagnosticsTests를 보강하고 README.md 및 docs/DEVELOPMENT.md 이력을 2026-04-16 00:15 (KST) 기준으로 갱신한다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_manifest_quality\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality\\ / dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter PptQualityGatePolicyTests -p:OutputPath=bin\\verify_ppt_manifest_quality_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality_tests\\ 포함 15건 통과
This commit is contained in:
12
README.md
12
README.md
@@ -15,6 +15,18 @@
|
|||||||
- 검증:
|
- 검증:
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_chat_width_wrap\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap\\` 경고 0 / 오류 0
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_chat_width_wrap\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap\\` 경고 0 / 오류 0
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_chat_width_wrap_tests\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap_tests\\` 통과 74
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_chat_width_wrap_tests\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap_tests\\` 통과 74
|
||||||
|
업데이트: 2026-04-16 00:15 (KST)
|
||||||
|
- PPT 템플릿 품질 게이트와 manifest 기반 추천 흐름을 함께 보강했습니다. `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`는 이제 `PPT quality` 점수뿐 아니라 `asset missing`, `master clone failed`, `color fallback`을 각각 구분해서 재루프 여부를 판단하고, 원인에 맞는 재생성 프롬프트를 만들도록 확장했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/PptxSkill.cs`는 템플릿 마스터 복제 실패를 `openxml_package_error`, `missing_slide_master`, `missing_slide_layout` 같은 진단 코드로 남기고, 결과 문자열에 `Template diagnostics:` 라인을 추가합니다. 이로써 이전처럼 모든 실패가 단순 `color fallback`으로만 보이지 않고, 실제 실패 원인을 로그와 품질 게이트가 함께 사용합니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`와 `src/AxCopilot/Assets/ppt/templates.manifest.json`은 `tone`, `density`, `fidelityTier`, `objectiveKeywords`, `audienceKeywords`, `supportsMasterClone` 메타데이터를 갖는 구조로 확장했습니다. 이제 템플릿 팩 선택 시 단순 고정 `PreferredTemplate`만 보는 대신 manifest 점수화로 추천 템플릿을 고르고, 결과에도 `Template recommendation:`을 남깁니다.
|
||||||
|
- 테스트:
|
||||||
|
- `src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs`
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs`
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs`
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxSkillTemplateDiagnosticsTests.cs`
|
||||||
|
- 검증:
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_manifest_quality\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality\\` 경고 0 / 오류 0
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests|PptxSkillTemplatePackTests|PptxSkillTemplateDiagnosticsTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests" -p:OutputPath=bin\\verify_ppt_manifest_quality_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality_tests\\` 통과 15
|
||||||
|
|
||||||
- 업데이트: 2026-04-15 22:45 (KST)
|
- 업데이트: 2026-04-15 22:45 (KST)
|
||||||
- HTML 보고서 뒤쪽으로 갈수록 폰트 크기와 카드 레이아웃이 흔들리던 raw body 호환 문제를 보강했습니다. `src/AxCopilot/Services/Agent/TemplateService.cs`에 `h4`, `dl`, `matrix`, `comparison`, `decision_matrix`, `board_report`, `metrics`, `roadmap` 같은 레거시 블록 전용 CSS를 추가해, 구조화 섹션이 아닌 자유 본문 HTML로 생성된 보고서도 앞부분과 같은 문서 톤을 유지하도록 맞췄습니다.
|
- HTML 보고서 뒤쪽으로 갈수록 폰트 크기와 카드 레이아웃이 흔들리던 raw body 호환 문제를 보강했습니다. `src/AxCopilot/Services/Agent/TemplateService.cs`에 `h4`, `dl`, `matrix`, `comparison`, `decision_matrix`, `board_report`, `metrics`, `roadmap` 같은 레거시 블록 전용 CSS를 추가해, 구조화 섹션이 아닌 자유 본문 HTML로 생성된 보고서도 앞부분과 같은 문서 톤을 유지하도록 맞췄습니다.
|
||||||
|
|||||||
@@ -1711,3 +1711,31 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
|||||||
- 검증:
|
- 검증:
|
||||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_chat_width_wrap\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap\\` 경고 0 / 오류 0
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_chat_width_wrap\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap\\` 경고 0 / 오류 0
|
||||||
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_chat_width_wrap_tests\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap_tests\\` 통과 74
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_chat_width_wrap_tests\\ -p:IntermediateOutputPath=obj\\verify_chat_width_wrap_tests\\` 통과 74
|
||||||
|
업데이트: 2026-04-16 00:15 (KST)
|
||||||
|
- PPT 템플릿 품질 보정 흐름을 manifest 중심으로 재구성했다.
|
||||||
|
- `src/AxCopilot/Services/Agent/PptQualityGatePolicy.cs`
|
||||||
|
- `PPT quality`, `Slide alerts`, `Needs work` 외에 `Template diagnostics:`와 `color fallback` 문구를 함께 파싱한다.
|
||||||
|
- `asset missing`, `master clone failed`, `color fallback` 중 하나라도 남으면 재생성 후보로 판단한다.
|
||||||
|
- 재생성 프롬프트를 원인별로 분기해 `document_plan` 선행 여부, manifest 템플릿/`template_pack` 재선택, `master cloned` 결과 강제 유도를 포함한다.
|
||||||
|
- `src/AxCopilot/Services/Agent/PptxSkill.cs`
|
||||||
|
- `TryCloneMasterFromTemplate(...)`를 `TemplateMasterCloneResult` 기반으로 바꿔 `openxml_package_error`, `missing_presentation_part`, `missing_slide_master`, `missing_slide_layout`, `clone_exception` 같은 진단 코드를 결과 문자열에 남긴다.
|
||||||
|
- 템플릿 복제 실패 시 `Template diagnostics:` 라인을 추가하고, `themeLabel`도 `master clone failed:<reason> -> color fallback` 형태로 출력한다.
|
||||||
|
- `template_pack` 사용 시 manifest 추천 결과를 먼저 계산하고, 추천된 템플릿 key와 `tone/density/fidelityTier/reason`를 출력에 포함한다.
|
||||||
|
- `src/AxCopilot/Services/Agent/PptxTemplateManifestCatalog.cs`
|
||||||
|
- manifest 엔트리를 `tone`, `density`, `aspectHint`, `fidelityTier`, `supportsMasterClone`, `objectiveKeywords`, `audienceKeywords` 메타데이터를 갖는 클래스로 확장했다.
|
||||||
|
- `RecommendTemplate(packName, objective, audience)`를 추가해 `packHints + objectiveKeywords + audienceKeywords + tags + fidelityTier` 점수로 가장 적합한 템플릿을 고른다.
|
||||||
|
- `src/AxCopilot/Assets/ppt/templates.manifest.json`
|
||||||
|
- 실제 자산 파일을 Unicode escape 기반 JSON으로 정리하고, 위 메타데이터를 모두 반영했다.
|
||||||
|
- 이 manifest가 이제 단순 파일 매핑이 아니라 내장 템플릿 pack 추천 테이블 역할까지 담당한다.
|
||||||
|
- 테스트:
|
||||||
|
- `src/AxCopilot.Tests/Services/PptQualityGatePolicyTests.cs`
|
||||||
|
- 저품질 + clone failure, asset missing 고득점 케이스, clean deck 통과, retry prompt 분기 검증을 추가했다.
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxTemplateManifestCatalogTests.cs`
|
||||||
|
- board/finance 신호에서 `core100`이 추천되는지와 manifest 메타데이터 로드를 검증한다.
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxSkillTemplatePackTests.cs`
|
||||||
|
- `Template recommendation: core100`이 실제 출력에 포함되는지 확인한다.
|
||||||
|
- `src/AxCopilot.Tests/Services/PptxSkillTemplateDiagnosticsTests.cs`
|
||||||
|
- 손상된 `theme_file` 입력에서 deck 생성은 유지하면서 `master clone failed` 진단을 남기는 회귀를 고정했다.
|
||||||
|
- 검증:
|
||||||
|
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_ppt_manifest_quality\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality\\` 경고 0 / 오류 0
|
||||||
|
- `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "PptQualityGatePolicyTests|PptxTemplateManifestCatalogTests|PptxSkillTemplatePackTests|PptxSkillTemplateDiagnosticsTests|PptxSkillAutoRepairTests|PptxSkillGoldenDeckTests" -p:OutputPath=bin\\verify_ppt_manifest_quality_tests\\ -p:IntermediateOutputPath=obj\\verify_ppt_manifest_quality_tests\\` 통과 15
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public class PptQualityGatePolicyTests
|
|||||||
public void TryAssess_ShouldRequestRetry_ForLowScoreDeck()
|
public void TryAssess_ShouldRequestRetry_ForLowScoreDeck()
|
||||||
{
|
{
|
||||||
var result = ToolResult.Ok(
|
var result = ToolResult.Ok(
|
||||||
"PPTX created: E:\\docu\\deck.pptx (10 slides, template:basic100 (color fallback), 16:9)\n" +
|
"PPTX created: E:\\docu\\deck.pptx (10 slides, template:basic100 (master clone failed:openxml_package_error -> color fallback), 16:9)\n" +
|
||||||
|
"Template diagnostics: template 'basic100' master clone failed (openxml_package_error). File contains corrupted data.\n" +
|
||||||
"PPT quality 64/100 | Strengths: Includes title slide | Slide alerts: 4 | Needs work: Too many slides are text-heavy.");
|
"PPT quality 64/100 | Strengths: Includes title slide | Slide alerts: 4 | Needs work: Too many slides are text-heavy.");
|
||||||
|
|
||||||
var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment);
|
var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment);
|
||||||
@@ -19,6 +20,22 @@ public class PptQualityGatePolicyTests
|
|||||||
assessment.Score.Should().Be(64);
|
assessment.Score.Should().Be(64);
|
||||||
assessment.SlideAlertCount.Should().Be(4);
|
assessment.SlideAlertCount.Should().Be(4);
|
||||||
assessment.HasColorFallback.Should().BeTrue();
|
assessment.HasColorFallback.Should().BeTrue();
|
||||||
|
assessment.HasMasterCloneFailure.Should().BeTrue();
|
||||||
|
assessment.RequiresRetry.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryAssess_ShouldRequestRetry_ForTemplateAssetMissingEvenWhenScoreIsHigh()
|
||||||
|
{
|
||||||
|
var result = ToolResult.Ok(
|
||||||
|
"PPTX created: E:\\docu\\deck.pptx (6 slides, template:core100 (asset missing -> built-in fallback), 16:9)\n" +
|
||||||
|
"Template diagnostics: 'core100' asset not found. Searched 3 candidate PPT directories.\n" +
|
||||||
|
"PPT quality 86/100 | Strengths: Clear structure | Needs work: none");
|
||||||
|
|
||||||
|
var parsed = PptQualityGatePolicy.TryAssess(result, out var assessment);
|
||||||
|
|
||||||
|
parsed.Should().BeTrue();
|
||||||
|
assessment.HasTemplateAssetMissing.Should().BeTrue();
|
||||||
assessment.RequiresRetry.Should().BeTrue();
|
assessment.RequiresRetry.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,20 +54,27 @@ public class PptQualityGatePolicyTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BuildRetryPrompt_ShouldRecommendDocumentPlan_WhenStorylineWasNotPrepared()
|
public void BuildRetryPrompt_ShouldRecommendDocumentPlan_AndManifestTemplateRecovery()
|
||||||
{
|
{
|
||||||
var assessment = new PptQualityGateAssessment(
|
var assessment = new PptQualityGateAssessment(
|
||||||
64,
|
64,
|
||||||
3,
|
3,
|
||||||
HasColorFallback: false,
|
HasColorFallback: true,
|
||||||
"Too many slides are text-heavy.",
|
HasTemplateAssetMissing: false,
|
||||||
|
HasMasterCloneFailure: true,
|
||||||
|
NeedsWork: "Too many slides are text-heavy.",
|
||||||
|
TemplateDiagnostics: "template 'basic100' master clone failed (openxml_package_error).",
|
||||||
RequiresRetry: true,
|
RequiresRetry: true,
|
||||||
Summary: "점수 64/100 (기준 80), 슬라이드 경고 3건");
|
Summary: "score 64/100 (target 80), 3 slide alerts, color fallback triggered, master clone failed");
|
||||||
|
|
||||||
var prompt = PptQualityGatePolicy.BuildRetryPrompt(assessment, "E:\\docu\\deck.pptx", documentPlanWasCalled: false);
|
var prompt = PptQualityGatePolicy.BuildRetryPrompt(
|
||||||
|
assessment,
|
||||||
|
"E:\\docu\\deck.pptx",
|
||||||
|
documentPlanWasCalled: false);
|
||||||
|
|
||||||
prompt.Should().Contain("document_plan");
|
prompt.Should().Contain("document_plan");
|
||||||
prompt.Should().Contain("pptx_create");
|
prompt.Should().Contain("pptx_create");
|
||||||
|
prompt.Should().Contain("master cloned");
|
||||||
prompt.Should().Contain("E:\\docu\\deck.pptx");
|
prompt.Should().Contain("E:\\docu\\deck.pptx");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AxCopilot.Tests.Services;
|
||||||
|
|
||||||
|
public class PptxSkillTemplateDiagnosticsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_WithCorruptedThemeFile_ShouldReportCloneFailureDiagnostics()
|
||||||
|
{
|
||||||
|
var workDir = Path.Combine(Path.GetTempPath(), "ax-pptx-diagnostics-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(workDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var brokenTemplatePath = Path.Combine(workDir, "broken-theme.pptx");
|
||||||
|
await File.WriteAllTextAsync(brokenTemplatePath, "not a real pptx");
|
||||||
|
|
||||||
|
var context = new AgentContext
|
||||||
|
{
|
||||||
|
WorkFolder = workDir,
|
||||||
|
Permission = "Auto",
|
||||||
|
OperationMode = "external",
|
||||||
|
};
|
||||||
|
|
||||||
|
var tool = new PptxSkill();
|
||||||
|
var args = JsonDocument.Parse(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"path": "diagnostic-deck.pptx",
|
||||||
|
"theme_file": "broken-theme.pptx",
|
||||||
|
"slides": [
|
||||||
|
{
|
||||||
|
"layout": "content",
|
||||||
|
"title": "Current State",
|
||||||
|
"body": "Issue summary\nConstraint\nNext action"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""").RootElement;
|
||||||
|
|
||||||
|
var result = await tool.ExecuteAsync(args, context, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
result.Output.Should().Contain("master clone failed");
|
||||||
|
result.Output.Should().Contain("Template diagnostics:");
|
||||||
|
result.Output.Should().Contain("theme_file");
|
||||||
|
File.Exists(Path.Combine(workDir, "diagnostic-deck.pptx")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(workDir))
|
||||||
|
Directory.Delete(workDir, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ public class PptxSkillTemplatePackTests
|
|||||||
|
|
||||||
result.Success.Should().BeTrue();
|
result.Success.Should().BeTrue();
|
||||||
result.Output.Should().Contain("Template pack: board");
|
result.Output.Should().Contain("Template pack: board");
|
||||||
|
result.Output.Should().Contain("Template recommendation: core100");
|
||||||
result.Output.Should().NotContain("asset missing");
|
result.Output.Should().NotContain("asset missing");
|
||||||
File.Exists(Path.Combine(workDir, "board-pack-deck.pptx")).Should().BeTrue();
|
File.Exists(Path.Combine(workDir, "board-pack-deck.pptx")).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,24 @@ public class PptxTemplateManifestCatalogTests
|
|||||||
var entry = PptxTemplateManifestCatalog.Resolve("basic100");
|
var entry = PptxTemplateManifestCatalog.Resolve("basic100");
|
||||||
|
|
||||||
entry.Should().NotBeNull();
|
entry.Should().NotBeNull();
|
||||||
entry!.FileName.Should().Be("BASIC100 기준 템플릿 V1.pptx");
|
entry!.Key.Should().Be("basic100");
|
||||||
entry.FallbackTheme.Should().Be("professional");
|
entry.FallbackTheme.Should().Be("professional");
|
||||||
|
entry.FidelityTier.Should().Be("premium");
|
||||||
|
entry.ObjectiveKeywords.Should().Contain("strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecommendTemplate_ShouldPreferBoardTemplate_ForBoardFinanceSignals()
|
||||||
|
{
|
||||||
|
var recommendation = PptxTemplateManifestCatalog.RecommendTemplate(
|
||||||
|
"board",
|
||||||
|
"board governance and finance review",
|
||||||
|
"executive committee");
|
||||||
|
|
||||||
|
recommendation.Should().NotBeNull();
|
||||||
|
recommendation!.Entry.Key.Should().Be("core100");
|
||||||
|
recommendation.Entry.FidelityTier.Should().Be("premium");
|
||||||
|
recommendation.Reason.Should().Contain("pack:board");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -2,75 +2,131 @@
|
|||||||
"templates": [
|
"templates": [
|
||||||
{
|
{
|
||||||
"key": "basic100",
|
"key": "basic100",
|
||||||
"fileName": "BASIC100 기준 템플릿 V1.pptx",
|
"fileName": "BASIC100 \uae30\uc900 \ud15c\ud50c\ub9bf V1.pptx",
|
||||||
"displayName": "BASIC100 기준 템플릿 V1",
|
"displayName": "BASIC100 template",
|
||||||
"fallbackTheme": "professional",
|
"fallbackTheme": "professional",
|
||||||
"aliases": ["basic", "consulting_basic"],
|
"aliases": ["basic", "consulting_basic"],
|
||||||
"tags": ["executive", "strategy", "consulting", "brand"],
|
"tags": ["executive", "strategy", "consulting", "brand"],
|
||||||
"packHints": ["strategy", "pmo", "sales", "operating_model"]
|
"packHints": ["strategy", "pmo", "sales", "operating_model"],
|
||||||
|
"tone": "consulting",
|
||||||
|
"density": "balanced",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "premium",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["strategy", "transformation", "operating model", "growth", "roadmap"],
|
||||||
|
"audienceKeywords": ["executive", "leadership", "steering", "pmo", "business"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "core100",
|
"key": "core100",
|
||||||
"fileName": "CORE100 기준템플릿 V1.pptx",
|
"fileName": "CORE100 \uae30\uc900\ud15c\ud50c\ub9bf V1.pptx",
|
||||||
"displayName": "CORE100 기준 템플릿 V1",
|
"displayName": "CORE100 template",
|
||||||
"fallbackTheme": "corporate",
|
"fallbackTheme": "corporate",
|
||||||
"aliases": ["core", "board_core"],
|
"aliases": ["core", "board_core"],
|
||||||
"tags": ["board", "governance", "finance", "corporate"],
|
"tags": ["board", "governance", "finance", "corporate"],
|
||||||
"packHints": ["board", "finance"]
|
"packHints": ["board", "finance"],
|
||||||
|
"tone": "corporate",
|
||||||
|
"density": "concise",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "premium",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["board", "governance", "finance", "budget", "investment"],
|
||||||
|
"audienceKeywords": ["board", "executive", "committee", "cfo", "directors"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "frame_blue",
|
"key": "frame_blue",
|
||||||
"fileName": "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx",
|
"fileName": "P01_01_\ud504\ub808\uc784\ub514\uc790\uc778_PPT\ud15c\ud50c\ub9bf_\ube14\ub8e8(PPT_\ud3b8\uc9d1\uc6a9).pptx",
|
||||||
"displayName": "프레임디자인 블루",
|
"displayName": "Blue frame template",
|
||||||
"fallbackTheme": "modern",
|
"fallbackTheme": "modern",
|
||||||
"aliases": ["blue_frame"],
|
"aliases": ["blue_frame"],
|
||||||
"tags": ["frame", "blue", "report", "proposal"],
|
"tags": ["frame", "blue", "report", "proposal"],
|
||||||
"packHints": ["sales", "operating_model"]
|
"packHints": ["sales", "operating_model"],
|
||||||
|
"tone": "proposal",
|
||||||
|
"density": "balanced",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "high",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["proposal", "operating model", "sales", "status", "report"],
|
||||||
|
"audienceKeywords": ["client", "sales", "project", "leadership"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "mr_ppt_01",
|
"key": "mr_ppt_01",
|
||||||
"fileName": "미스터 피피티 템플릿 01_원본.pptx",
|
"fileName": "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 01_\uc6d0\ubcf8.pptx",
|
||||||
"displayName": "미스터 피피티 템플릿 01",
|
"displayName": "Mr PPT 01",
|
||||||
"fallbackTheme": "modern",
|
"fallbackTheme": "modern",
|
||||||
"aliases": ["mrppt01"],
|
"aliases": ["mrppt01"],
|
||||||
"tags": ["creative", "proposal", "sales"],
|
"tags": ["creative", "proposal", "sales"],
|
||||||
"packHints": ["sales"]
|
"packHints": ["sales"],
|
||||||
|
"tone": "creative",
|
||||||
|
"density": "balanced",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "standard",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["proposal", "pitch", "sales", "commercial"],
|
||||||
|
"audienceKeywords": ["client", "sales", "commercial"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "mr_ppt_02",
|
"key": "mr_ppt_02",
|
||||||
"fileName": "미스터 피피티 템플릿 02_원본.pptx",
|
"fileName": "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 02_\uc6d0\ubcf8.pptx",
|
||||||
"displayName": "미스터 피피티 템플릿 02",
|
"displayName": "Mr PPT 02",
|
||||||
"fallbackTheme": "vibrant",
|
"fallbackTheme": "vibrant",
|
||||||
"aliases": ["mrppt02"],
|
"aliases": ["mrppt02"],
|
||||||
"tags": ["creative", "marketing", "proposal"],
|
"tags": ["creative", "marketing", "proposal"],
|
||||||
"packHints": ["sales"]
|
"packHints": ["sales"],
|
||||||
|
"tone": "bold",
|
||||||
|
"density": "condensed",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "standard",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["marketing", "launch", "brand", "proposal"],
|
||||||
|
"audienceKeywords": ["marketing", "sales", "brand"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "mr_ppt_03",
|
"key": "mr_ppt_03",
|
||||||
"fileName": "미스터 피피티 03_원본.pptx",
|
"fileName": "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 03_\uc6d0\ubcf8.pptx",
|
||||||
"displayName": "미스터 피피티 템플릿 03",
|
"displayName": "Mr PPT 03",
|
||||||
"fallbackTheme": "minimal",
|
"fallbackTheme": "minimal",
|
||||||
"aliases": ["mrppt03"],
|
"aliases": ["mrppt03"],
|
||||||
"tags": ["minimal", "analysis", "report"],
|
"tags": ["minimal", "analysis", "report"],
|
||||||
"packHints": ["strategy", "finance"]
|
"packHints": ["strategy", "finance"],
|
||||||
|
"tone": "analytical",
|
||||||
|
"density": "concise",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "high",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["analysis", "review", "report", "diagnostic"],
|
||||||
|
"audienceKeywords": ["strategy", "finance", "leadership"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "mr_ppt_04",
|
"key": "mr_ppt_04",
|
||||||
"fileName": "미스터 피피티 템플릿 04_원본.pptx",
|
"fileName": "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 04_\uc6d0\ubcf8.pptx",
|
||||||
"displayName": "미스터 피피티 템플릿 04",
|
"displayName": "Mr PPT 04",
|
||||||
"fallbackTheme": "slate",
|
"fallbackTheme": "slate",
|
||||||
"aliases": ["mrppt04"],
|
"aliases": ["mrppt04"],
|
||||||
"tags": ["dark", "executive", "review"],
|
"tags": ["dark", "executive", "review"],
|
||||||
"packHints": ["board", "finance"]
|
"packHints": ["board", "finance"],
|
||||||
|
"tone": "executive",
|
||||||
|
"density": "condensed",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "high",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["board review", "risk", "finance", "committee"],
|
||||||
|
"audienceKeywords": ["board", "executive", "cfo"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "mr_ppt_05",
|
"key": "mr_ppt_05",
|
||||||
"fileName": "미스터 피피티 템플릿_05_원본.pptx",
|
"fileName": "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf_05_\uc6d0\ubcf8.pptx",
|
||||||
"displayName": "미스터 피피티 템플릿 05",
|
"displayName": "Mr PPT 05",
|
||||||
"fallbackTheme": "ocean",
|
"fallbackTheme": "ocean",
|
||||||
"aliases": ["mrppt05"],
|
"aliases": ["mrppt05"],
|
||||||
"tags": ["blue", "modern", "report"],
|
"tags": ["blue", "modern", "report"],
|
||||||
"packHints": ["pmo", "operating_model"]
|
"packHints": ["pmo", "operating_model"],
|
||||||
|
"tone": "operational",
|
||||||
|
"density": "balanced",
|
||||||
|
"aspectHint": "widescreen",
|
||||||
|
"fidelityTier": "high",
|
||||||
|
"supportsMasterClone": true,
|
||||||
|
"objectiveKeywords": ["pmo", "roadmap", "status", "operating model"],
|
||||||
|
"audienceKeywords": ["pmo", "operations", "delivery", "steering"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
@@ -6,7 +8,10 @@ public sealed record PptQualityGateAssessment(
|
|||||||
int Score,
|
int Score,
|
||||||
int SlideAlertCount,
|
int SlideAlertCount,
|
||||||
bool HasColorFallback,
|
bool HasColorFallback,
|
||||||
|
bool HasTemplateAssetMissing,
|
||||||
|
bool HasMasterCloneFailure,
|
||||||
string NeedsWork,
|
string NeedsWork,
|
||||||
|
string TemplateDiagnostics,
|
||||||
bool RequiresRetry,
|
bool RequiresRetry,
|
||||||
string Summary);
|
string Summary);
|
||||||
|
|
||||||
@@ -17,7 +22,7 @@ public static class PptQualityGatePolicy
|
|||||||
|
|
||||||
public static bool TryAssess(ToolResult result, out PptQualityGateAssessment assessment, int minScore = DefaultMinScore)
|
public static bool TryAssess(ToolResult result, out PptQualityGateAssessment assessment, int minScore = DefaultMinScore)
|
||||||
{
|
{
|
||||||
assessment = new PptQualityGateAssessment(0, 0, false, "", false, "");
|
assessment = new PptQualityGateAssessment(0, 0, false, false, false, "", "", false, "");
|
||||||
if (!result.Success || string.IsNullOrWhiteSpace(result.Output))
|
if (!result.Success || string.IsNullOrWhiteSpace(result.Output))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -28,22 +33,41 @@ public static class PptQualityGatePolicy
|
|||||||
|
|
||||||
var alertMatch = Regex.Match(output, @"Slide alerts:\s*(?<count>\d+)", RegexOptions.IgnoreCase);
|
var alertMatch = Regex.Match(output, @"Slide alerts:\s*(?<count>\d+)", RegexOptions.IgnoreCase);
|
||||||
var needsWorkMatch = Regex.Match(output, @"Needs work:\s*(?<value>.+?)(?:\||\r?\n|$)", RegexOptions.IgnoreCase);
|
var needsWorkMatch = Regex.Match(output, @"Needs work:\s*(?<value>.+?)(?:\||\r?\n|$)", RegexOptions.IgnoreCase);
|
||||||
|
var templateDiagnostics = ReadTemplateDiagnostics(output);
|
||||||
|
|
||||||
var slideAlerts = alertMatch.Success && int.TryParse(alertMatch.Groups["count"].Value, out var parsedAlerts)
|
var slideAlerts = alertMatch.Success && int.TryParse(alertMatch.Groups["count"].Value, out var parsedAlerts)
|
||||||
? parsedAlerts
|
? parsedAlerts
|
||||||
: 0;
|
: 0;
|
||||||
var needsWork = needsWorkMatch.Success ? needsWorkMatch.Groups["value"].Value.Trim() : "";
|
var needsWork = needsWorkMatch.Success ? needsWorkMatch.Groups["value"].Value.Trim() : "";
|
||||||
var hasColorFallback = output.Contains("color fallback", StringComparison.OrdinalIgnoreCase);
|
var hasColorFallback = output.Contains("color fallback", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var hasTemplateAssetMissing = output.Contains("asset missing", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
templateDiagnostics.Contains("asset not found", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var hasMasterCloneFailure = output.Contains("master clone failed", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
templateDiagnostics.Contains("master clone failed", StringComparison.OrdinalIgnoreCase);
|
||||||
var requiresRetry = score < minScore
|
var requiresRetry = score < minScore
|
||||||
|| slideAlerts > 0
|
|| slideAlerts > 0
|
||||||
|
|| hasColorFallback
|
||||||
|
|| hasTemplateAssetMissing
|
||||||
|
|| hasMasterCloneFailure
|
||||||
|| (!string.IsNullOrWhiteSpace(needsWork)
|
|| (!string.IsNullOrWhiteSpace(needsWork)
|
||||||
&& !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase));
|
&& !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase));
|
||||||
var summary = BuildSummary(score, slideAlerts, hasColorFallback, needsWork, minScore);
|
var summary = BuildSummary(
|
||||||
|
score,
|
||||||
|
slideAlerts,
|
||||||
|
hasColorFallback,
|
||||||
|
hasTemplateAssetMissing,
|
||||||
|
hasMasterCloneFailure,
|
||||||
|
needsWork,
|
||||||
|
minScore);
|
||||||
|
|
||||||
assessment = new PptQualityGateAssessment(
|
assessment = new PptQualityGateAssessment(
|
||||||
score,
|
score,
|
||||||
slideAlerts,
|
slideAlerts,
|
||||||
hasColorFallback,
|
hasColorFallback,
|
||||||
|
hasTemplateAssetMissing,
|
||||||
|
hasMasterCloneFailure,
|
||||||
needsWork,
|
needsWork,
|
||||||
|
templateDiagnostics,
|
||||||
requiresRetry,
|
requiresRetry,
|
||||||
summary);
|
summary);
|
||||||
return true;
|
return true;
|
||||||
@@ -54,39 +78,75 @@ public static class PptQualityGatePolicy
|
|||||||
string? filePath,
|
string? filePath,
|
||||||
bool documentPlanWasCalled)
|
bool documentPlanWasCalled)
|
||||||
{
|
{
|
||||||
var target = string.IsNullOrWhiteSpace(filePath) ? "방금 생성한 PPT 파일" : $"'{filePath}'";
|
var target = string.IsNullOrWhiteSpace(filePath) ? "the previous PPT output" : $"'{filePath}'";
|
||||||
var planStep = documentPlanWasCalled
|
var planStep = documentPlanWasCalled
|
||||||
? "기존 스토리라인을 더 날카롭게 다듬고 같은 파일 경로로 다시 생성하세요."
|
? "Reuse the current storyline, but tighten the slide narrative and regenerate to the same output path."
|
||||||
: "먼저 document_plan으로 스토리라인을 재정리한 뒤, 같은 파일 경로로 pptx_create를 다시 호출하세요.";
|
: "Call document_plan first to rebuild the storyline, then regenerate with pptx_create to the same output path.";
|
||||||
var templateStep = assessment.HasColorFallback
|
var templateStep = assessment.HasTemplateAssetMissing
|
||||||
? "템플릿 마스터 복제가 끝까지 적용되지 않았으니 template/template_pack 선택을 다시 점검하고 브랜드 템플릿을 우선 사용하세요."
|
? "Use only bundled manifest templates or template_pack values. Do not reference a missing external PPT asset."
|
||||||
: "template_pack(strategy/board/pmo/finance/sales/operating_model) 또는 적절한 브랜드 템플릿을 적극 사용하세요.";
|
: assessment.HasMasterCloneFailure
|
||||||
var needsWorkLine = string.IsNullOrWhiteSpace(assessment.NeedsWork)
|
? "Switch to a bundled manifest template or template_pack that can finish with 'master cloned'. Do not stop on color fallback."
|
||||||
? "- 주요 보완: 슬라이드당 메시지를 하나로 줄이고, 근거 슬라이드와 결론 슬라이드의 대비를 분명히 하세요.\n"
|
: assessment.HasColorFallback
|
||||||
: $"- 주요 보완: {assessment.NeedsWork}\n";
|
? "Prefer a bundled manifest template or template_pack and regenerate until the result reports 'master cloned' instead of 'color fallback'."
|
||||||
|
: "Use the most suitable template_pack (strategy, board, pmo, finance, sales, operating_model) to strengthen layout fidelity.";
|
||||||
|
var needsWorkLine = string.IsNullOrWhiteSpace(assessment.NeedsWork) ||
|
||||||
|
string.Equals(assessment.NeedsWork, "none", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "- Main repair goal: reduce text density, sharpen the headline, and add evidence or takeaway slides where needed.\n"
|
||||||
|
: $"- Main repair goal: {assessment.NeedsWork}\n";
|
||||||
|
var diagnosticsLine = string.IsNullOrWhiteSpace(assessment.TemplateDiagnostics)
|
||||||
|
? string.Empty
|
||||||
|
: $"- Template diagnostics: {assessment.TemplateDiagnostics}\n";
|
||||||
|
|
||||||
return "[System:PptQualityGate] 방금 생성한 PPT 품질이 기준 미만입니다.\n" +
|
return "[System:PptQualityGate] The previous PPT did not meet the minimum quality bar.\n" +
|
||||||
$"- 대상 파일: {target}\n" +
|
$"- Target file: {target}\n" +
|
||||||
$"- 품질 요약: {assessment.Summary}\n" +
|
$"- Quality summary: {assessment.Summary}\n" +
|
||||||
|
diagnosticsLine +
|
||||||
needsWorkLine +
|
needsWorkLine +
|
||||||
"- 필수 개선 방향:\n" +
|
"- Required improvements:\n" +
|
||||||
" 1. 한 슬라이드 한 메시지 원칙으로 텍스트 밀도를 낮추세요.\n" +
|
" 1. Keep one headline per slide and compress overly dense text into short evidence-backed bullets.\n" +
|
||||||
" 2. Executive Summary → Options/Comparison → Recommendation → Roadmap → Appendix/Evidence 흐름을 분명히 하세요.\n" +
|
" 2. Make the flow explicit across Executive Summary, Options/Comparison, Recommendation, Roadmap, and Appendix/Evidence.\n" +
|
||||||
" 3. 표/차트/비교 슬라이드로 정량 근거를 늘리고, 헤드라인을 더 결론형 문장으로 바꾸세요.\n" +
|
" 3. Turn long narrative sections into comparison, KPI, chart, or roadmap layouts when possible.\n" +
|
||||||
$" 4. {templateStep}\n" +
|
$" 4. {templateStep}\n" +
|
||||||
$" 5. {planStep}\n" +
|
$" 5. {planStep}\n" +
|
||||||
"개선된 버전을 만든 뒤 다시 품질 요약이 좋아졌는지 확인하세요.";
|
"Regenerate the deck and verify that the reported quality summary improves.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildSummary(int score, int slideAlerts, bool hasColorFallback, string needsWork, int minScore)
|
private static string ReadTemplateDiagnostics(string output)
|
||||||
{
|
{
|
||||||
var parts = new List<string> { $"점수 {score}/100 (기준 {minScore})" };
|
var matches = Regex.Matches(
|
||||||
|
output,
|
||||||
|
@"^Template diagnostics:\s*(?<value>.+)$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||||
|
if (matches.Count == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return string.Join(
|
||||||
|
" | ",
|
||||||
|
matches
|
||||||
|
.Select(match => match.Groups["value"].Value.Trim())
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSummary(
|
||||||
|
int score,
|
||||||
|
int slideAlerts,
|
||||||
|
bool hasColorFallback,
|
||||||
|
bool hasTemplateAssetMissing,
|
||||||
|
bool hasMasterCloneFailure,
|
||||||
|
string needsWork,
|
||||||
|
int minScore)
|
||||||
|
{
|
||||||
|
var parts = new List<string> { $"score {score}/100 (target {minScore})" };
|
||||||
if (slideAlerts > 0)
|
if (slideAlerts > 0)
|
||||||
parts.Add($"슬라이드 경고 {slideAlerts}건");
|
parts.Add($"{slideAlerts} slide alerts");
|
||||||
if (hasColorFallback)
|
if (hasColorFallback)
|
||||||
parts.Add("template color fallback 발생");
|
parts.Add("color fallback triggered");
|
||||||
|
if (hasTemplateAssetMissing)
|
||||||
|
parts.Add("template asset missing");
|
||||||
|
if (hasMasterCloneFailure)
|
||||||
|
parts.Add("master clone failed");
|
||||||
if (!string.IsNullOrWhiteSpace(needsWork) && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrWhiteSpace(needsWork) && !string.Equals(needsWork, "none", StringComparison.OrdinalIgnoreCase))
|
||||||
parts.Add($"Needs work: {needsWork}");
|
parts.Add($"needs work: {needsWork}");
|
||||||
return string.Join(", ", parts);
|
return string.Join(", ", parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,19 @@ public class PptxSkill : IAgentTool
|
|||||||
// ── 통합 테마 레코드 ──────────────────────────────────────────────────────
|
// ── 통합 테마 레코드 ──────────────────────────────────────────────────────
|
||||||
private record FullTheme(ThemeColors Colors, ThemeLayout Layout);
|
private record FullTheme(ThemeColors Colors, ThemeLayout Layout);
|
||||||
|
|
||||||
|
private sealed record TemplateMasterCloneResult(
|
||||||
|
bool Success,
|
||||||
|
SlideLayoutPart? LayoutPart,
|
||||||
|
string Status,
|
||||||
|
string Detail)
|
||||||
|
{
|
||||||
|
public static TemplateMasterCloneResult Succeeded(SlideLayoutPart layoutPart)
|
||||||
|
=> new(true, layoutPart, "ok", "");
|
||||||
|
|
||||||
|
public static TemplateMasterCloneResult Failed(string status, string detail)
|
||||||
|
=> new(false, null, status, detail);
|
||||||
|
}
|
||||||
|
|
||||||
private static FullTheme ResolveTemplateFallbackTheme(
|
private static FullTheme ResolveTemplateFallbackTheme(
|
||||||
string templateName,
|
string templateName,
|
||||||
PptxTemplateResolution? resolution,
|
PptxTemplateResolution? resolution,
|
||||||
@@ -549,8 +562,14 @@ public class PptxSkill : IAgentTool
|
|||||||
: (!hasExplicitTheme && !hasExplicitTemplate && !hasThemeFile
|
: (!hasExplicitTheme && !hasExplicitTemplate && !hasThemeFile
|
||||||
? PptxTemplatePackRegistry.Suggest(renderDeck?.Objective ?? objective, renderDeck?.Audience ?? audience)
|
? PptxTemplatePackRegistry.Suggest(renderDeck?.Objective ?? objective, renderDeck?.Audience ?? audience)
|
||||||
: null);
|
: null);
|
||||||
|
var templatePackRecommendation = templatePack != null
|
||||||
|
? PptxTemplateManifestCatalog.RecommendTemplate(
|
||||||
|
templatePack.Name,
|
||||||
|
renderDeck?.Objective ?? objective,
|
||||||
|
renderDeck?.Audience ?? audience)
|
||||||
|
: null;
|
||||||
var templatePackName = templatePack?.Name;
|
var templatePackName = templatePack?.Name;
|
||||||
var packTemplateName = templatePack?.PreferredTemplate;
|
var packTemplateName = templatePackRecommendation?.Entry.Key ?? templatePack?.PreferredTemplate;
|
||||||
PptxTemplateResolution? explicitTemplateResolution = null;
|
PptxTemplateResolution? explicitTemplateResolution = null;
|
||||||
PptxTemplateResolution? packTemplateResolution = null;
|
PptxTemplateResolution? packTemplateResolution = null;
|
||||||
|
|
||||||
@@ -703,6 +722,7 @@ public class PptxSkill : IAgentTool
|
|||||||
{
|
{
|
||||||
var cloneInfo_srcLabel = templatePptxPath != null ? Path.GetFileName(templatePptxPath) : null;
|
var cloneInfo_srcLabel = templatePptxPath != null ? Path.GetFileName(templatePptxPath) : null;
|
||||||
var cloneInfo_cloned = false;
|
var cloneInfo_cloned = false;
|
||||||
|
TemplateMasterCloneResult? templateCloneResult = null;
|
||||||
int slideCount_final = 0;
|
int slideCount_final = 0;
|
||||||
|
|
||||||
using (var pres = PresentationDocument.Create(fullPath, PresentationDocumentType.Presentation))
|
using (var pres = PresentationDocument.Create(fullPath, PresentationDocumentType.Presentation))
|
||||||
@@ -714,9 +734,69 @@ public class PptxSkill : IAgentTool
|
|||||||
var clonedFromTemplate = false;
|
var clonedFromTemplate = false;
|
||||||
|
|
||||||
// ── 마스터 복제 또는 자체 생성 ─────────────────────────────────
|
// ── 마스터 복제 또는 자체 생성 ─────────────────────────────────
|
||||||
if (templatePptxPath != null && TryCloneMasterFromTemplate(templatePptxPath, presPart, out layoutPart!))
|
if (templatePptxPath != null)
|
||||||
{
|
{
|
||||||
clonedFromTemplate = true;
|
templateCloneResult = TryCloneMasterFromTemplate(templatePptxPath, presPart);
|
||||||
|
if (templateCloneResult.Success && templateCloneResult.LayoutPart != null)
|
||||||
|
{
|
||||||
|
layoutPart = templateCloneResult.LayoutPart;
|
||||||
|
clonedFromTemplate = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 자체 마스터 생성 (기존 방식)
|
||||||
|
var masterPart = presPart.AddNewPart<SlideMasterPart>();
|
||||||
|
masterPart.SlideMaster = new SlideMaster(
|
||||||
|
new CommonSlideData(new ShapeTree(
|
||||||
|
new P.NonVisualGroupShapeProperties(
|
||||||
|
new P.NonVisualDrawingProperties { Id = 1, Name = "" },
|
||||||
|
new P.NonVisualGroupShapeDrawingProperties(),
|
||||||
|
new ApplicationNonVisualDrawingProperties()),
|
||||||
|
new GroupShapeProperties(new A.TransformGroup()))),
|
||||||
|
new P.ColorMap
|
||||||
|
{
|
||||||
|
Background1 = A.ColorSchemeIndexValues.Light1,
|
||||||
|
Text1 = A.ColorSchemeIndexValues.Dark1,
|
||||||
|
Background2 = A.ColorSchemeIndexValues.Light2,
|
||||||
|
Text2 = A.ColorSchemeIndexValues.Dark2,
|
||||||
|
Accent1 = A.ColorSchemeIndexValues.Accent1,
|
||||||
|
Accent2 = A.ColorSchemeIndexValues.Accent2,
|
||||||
|
Accent3 = A.ColorSchemeIndexValues.Accent3,
|
||||||
|
Accent4 = A.ColorSchemeIndexValues.Accent4,
|
||||||
|
Accent5 = A.ColorSchemeIndexValues.Accent5,
|
||||||
|
Accent6 = A.ColorSchemeIndexValues.Accent6,
|
||||||
|
Hyperlink = A.ColorSchemeIndexValues.Hyperlink,
|
||||||
|
FollowedHyperlink = A.ColorSchemeIndexValues.FollowedHyperlink,
|
||||||
|
},
|
||||||
|
new SlideLayoutIdList());
|
||||||
|
|
||||||
|
layoutPart = masterPart.AddNewPart<SlideLayoutPart>();
|
||||||
|
layoutPart.SlideLayout = new SlideLayout(
|
||||||
|
new CommonSlideData(new ShapeTree(
|
||||||
|
new P.NonVisualGroupShapeProperties(
|
||||||
|
new P.NonVisualDrawingProperties { Id = 1, Name = "" },
|
||||||
|
new P.NonVisualGroupShapeDrawingProperties(),
|
||||||
|
new ApplicationNonVisualDrawingProperties()),
|
||||||
|
new GroupShapeProperties(new A.TransformGroup()))))
|
||||||
|
{ Type = SlideLayoutValues.Blank, Preserve = true };
|
||||||
|
layoutPart.AddPart(masterPart);
|
||||||
|
|
||||||
|
masterPart.SlideMaster.SlideLayoutIdList!.AppendChild(new SlideLayoutId
|
||||||
|
{
|
||||||
|
Id = 256U,
|
||||||
|
RelationshipId = masterPart.GetIdOfPart(layoutPart)
|
||||||
|
});
|
||||||
|
|
||||||
|
// ThemePart 생성
|
||||||
|
var themePart = masterPart.AddNewPart<ThemePart>();
|
||||||
|
themePart.Theme = CreateOfficeTheme(fullTheme.Colors, "AX Theme");
|
||||||
|
|
||||||
|
presPart.Presentation.Append(new SlideMasterIdList(new SlideMasterId
|
||||||
|
{
|
||||||
|
Id = 2147483648U,
|
||||||
|
RelationshipId = presPart.GetIdOfPart(masterPart)
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1008,6 +1088,7 @@ public class PptxSkill : IAgentTool
|
|||||||
RepairContentTypes(fullPath);
|
RepairContentTypes(fullPath);
|
||||||
|
|
||||||
var templateSourceName = templateName ?? packTemplateName;
|
var templateSourceName = templateName ?? packTemplateName;
|
||||||
|
var fallbackLabel = FormatTemplateFallbackLabel(templateCloneResult);
|
||||||
var themeLabel = cloneInfo_cloned
|
var themeLabel = cloneInfo_cloned
|
||||||
? templateSourceName != null
|
? templateSourceName != null
|
||||||
? $"template:{templateSourceName} (master cloned{(cloneAll ? " + slides cloned" : "")})"
|
? $"template:{templateSourceName} (master cloned{(cloneAll ? " + slides cloned" : "")})"
|
||||||
@@ -1017,34 +1098,36 @@ public class PptxSkill : IAgentTool
|
|||||||
: !string.IsNullOrWhiteSpace(templateName)
|
: !string.IsNullOrWhiteSpace(templateName)
|
||||||
? templatePptxPath == null
|
? templatePptxPath == null
|
||||||
? $"template:{templateName} (asset missing -> built-in fallback)"
|
? $"template:{templateName} (asset missing -> built-in fallback)"
|
||||||
: $"template:{templateName} (color fallback)"
|
: $"template:{templateName} ({fallbackLabel})"
|
||||||
: templatePptxPath != null && !string.IsNullOrWhiteSpace(packTemplateName)
|
: templatePptxPath != null && !string.IsNullOrWhiteSpace(packTemplateName)
|
||||||
? $"template:{packTemplateName} (color fallback)"
|
? $"template:{packTemplateName} ({fallbackLabel})"
|
||||||
: !string.IsNullOrWhiteSpace(packTemplateName)
|
: !string.IsNullOrWhiteSpace(packTemplateName)
|
||||||
? $"template:{packTemplateName} (asset missing -> pack fallback)"
|
? $"template:{packTemplateName} (asset missing -> pack fallback)"
|
||||||
: templatePptxPath != null
|
: templatePptxPath != null
|
||||||
? $"theme_file:{cloneInfo_srcLabel} (color fallback)"
|
? $"theme_file:{cloneInfo_srcLabel} ({fallbackLabel})"
|
||||||
: theme;
|
: theme;
|
||||||
var outputParts = new List<string>
|
var outputParts = new List<string>
|
||||||
{
|
{
|
||||||
$"PPTX created: {fullPath} ({slideCount_final} slides, {themeLabel}, {(isWide ? "16:9" : "4:3")})"
|
$"PPTX created: {fullPath} ({slideCount_final} slides, {themeLabel}, {(isWide ? "16:9" : "4:3")})"
|
||||||
};
|
};
|
||||||
if (explicitTemplateResolution != null && explicitTemplateResolution.Status == "asset_missing")
|
outputParts.AddRange(BuildTemplateDiagnosticsLines(
|
||||||
{
|
explicitTemplateResolution,
|
||||||
outputParts.Add(
|
packTemplateResolution,
|
||||||
$"Template diagnostics: '{explicitTemplateResolution.RequestedName}' asset not found. " +
|
templateCloneResult,
|
||||||
$"Searched {explicitTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
|
templateName,
|
||||||
}
|
packTemplateName,
|
||||||
else if (packTemplateResolution != null && packTemplateResolution.Status == "asset_missing")
|
cloneInfo_srcLabel));
|
||||||
{
|
|
||||||
outputParts.Add(
|
|
||||||
$"Template diagnostics: pack template '{packTemplateResolution.RequestedName}' asset not found. " +
|
|
||||||
$"Searched {packTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
|
|
||||||
}
|
|
||||||
if (renderDeck != null)
|
if (renderDeck != null)
|
||||||
outputParts.Add(renderDeck.ToToolSummary());
|
outputParts.Add(renderDeck.ToToolSummary());
|
||||||
if (!string.IsNullOrWhiteSpace(templatePackName))
|
if (!string.IsNullOrWhiteSpace(templatePackName))
|
||||||
outputParts.Add($"Template pack: {templatePackName}");
|
outputParts.Add($"Template pack: {templatePackName}");
|
||||||
|
if (templatePackRecommendation != null)
|
||||||
|
{
|
||||||
|
outputParts.Add(
|
||||||
|
$"Template recommendation: {templatePackRecommendation.Entry.Key} " +
|
||||||
|
$"(tone={templatePackRecommendation.Entry.Tone}, density={templatePackRecommendation.Entry.Density}, " +
|
||||||
|
$"fidelity={templatePackRecommendation.Entry.FidelityTier}, reason={templatePackRecommendation.Reason})");
|
||||||
|
}
|
||||||
if (deckReview == null && hasSlidesArray)
|
if (deckReview == null && hasSlidesArray)
|
||||||
{
|
{
|
||||||
deckReview = DeckQualityReviewService.ReviewDeck(
|
deckReview = DeckQualityReviewService.ReviewDeck(
|
||||||
@@ -2499,28 +2582,80 @@ public class PptxSkill : IAgentTool
|
|||||||
notesPart.NotesSlide.Save();
|
notesPart.NotesSlide.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static A.Theme CreateOfficeTheme(ThemeColors colors, string themeName)
|
||||||
|
{
|
||||||
|
var palette = colors;
|
||||||
|
return new A.Theme(
|
||||||
|
new A.ThemeElements(
|
||||||
|
new A.ColorScheme(
|
||||||
|
new A.Dark1Color(new A.SystemColor { Val = A.SystemColorValues.WindowText, LastColor = "000000" }),
|
||||||
|
new A.Light1Color(new A.SystemColor { Val = A.SystemColorValues.Window, LastColor = "FFFFFF" }),
|
||||||
|
new A.Dark2Color(new A.RgbColorModelHex { Val = palette.TextDark.TrimStart('#') }),
|
||||||
|
new A.Light2Color(new A.RgbColorModelHex { Val = palette.BgAlt.TrimStart('#') }),
|
||||||
|
new A.Accent1Color(new A.RgbColorModelHex { Val = palette.Primary.TrimStart('#') }),
|
||||||
|
new A.Accent2Color(new A.RgbColorModelHex { Val = palette.Accent.TrimStart('#') }),
|
||||||
|
new A.Accent3Color(new A.RgbColorModelHex { Val = palette.HeaderBg.TrimStart('#') }),
|
||||||
|
new A.Accent4Color(new A.RgbColorModelHex { Val = palette.Accent.TrimStart('#') }),
|
||||||
|
new A.Accent5Color(new A.RgbColorModelHex { Val = palette.Primary.TrimStart('#') }),
|
||||||
|
new A.Accent6Color(new A.RgbColorModelHex { Val = palette.Accent.TrimStart('#') }),
|
||||||
|
new A.Hyperlink(new A.RgbColorModelHex { Val = "0563C1" }),
|
||||||
|
new A.FollowedHyperlinkColor(new A.RgbColorModelHex { Val = "954F72" })
|
||||||
|
) { Name = themeName },
|
||||||
|
new A.FontScheme(
|
||||||
|
new A.MajorFont(new A.LatinFont { Typeface = "Noto Sans KR" },
|
||||||
|
new A.EastAsianFont { Typeface = "Noto Sans KR" },
|
||||||
|
new A.ComplexScriptFont { Typeface = "Noto Sans KR" }),
|
||||||
|
new A.MinorFont(new A.LatinFont { Typeface = "Noto Sans KR" },
|
||||||
|
new A.EastAsianFont { Typeface = "Noto Sans KR" },
|
||||||
|
new A.ComplexScriptFont { Typeface = "Noto Sans KR" })
|
||||||
|
) { Name = themeName },
|
||||||
|
new A.FormatScheme(
|
||||||
|
new A.FillStyleList(
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor }),
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor }),
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor })),
|
||||||
|
new A.LineStyleList(
|
||||||
|
new A.Outline(new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor })) { Width = 9525 },
|
||||||
|
new A.Outline(new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor })) { Width = 9525 },
|
||||||
|
new A.Outline(new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor })) { Width = 9525 }),
|
||||||
|
new A.EffectStyleList(
|
||||||
|
new A.EffectStyle(new A.EffectList()),
|
||||||
|
new A.EffectStyle(new A.EffectList()),
|
||||||
|
new A.EffectStyle(new A.EffectList())),
|
||||||
|
new A.BackgroundFillStyleList(
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor }),
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor }),
|
||||||
|
new A.SolidFill(new A.SchemeColor { Val = A.SchemeColorValues.PhColor }))
|
||||||
|
) { Name = themeName }
|
||||||
|
),
|
||||||
|
new A.ObjectDefaults(),
|
||||||
|
new A.ExtraColorSchemeList())
|
||||||
|
{ Name = themeName };
|
||||||
|
}
|
||||||
|
|
||||||
// ── 고품질 템플릿에서 마스터/레이아웃/테마 복제 ──────────────────────────────
|
// ── 고품질 템플릿에서 마스터/레이아웃/테마 복제 ──────────────────────────────
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 기존 .pptx 템플릿에서 SlideMaster, SlideLayout, ThemePart를 통째로 복제합니다.
|
/// 기존 .pptx 템플릿에서 SlideMaster, SlideLayout, ThemePart를 통째로 복제합니다.
|
||||||
/// 복제 성공 시 layoutPart에 첫 번째 레이아웃을 반환하고 true.
|
/// 복제 성공 시 layoutPart에 첫 번째 레이아웃을 반환하고 true.
|
||||||
/// 실패 시 false → 호출부에서 자체 마스터를 생성합니다 (색상 추출 폴백).
|
/// 실패 시 false → 호출부에서 자체 마스터를 생성합니다 (색상 추출 폴백).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryCloneMasterFromTemplate(
|
private static TemplateMasterCloneResult TryCloneMasterFromTemplate(
|
||||||
string templatePath,
|
string templatePath,
|
||||||
PresentationPart targetPresPart,
|
PresentationPart targetPresPart)
|
||||||
out SlideLayoutPart? layoutPart)
|
|
||||||
{
|
{
|
||||||
layoutPart = null;
|
if (!File.Exists(templatePath))
|
||||||
if (!File.Exists(templatePath)) return false;
|
return TemplateMasterCloneResult.Failed("file_missing", "The template file does not exist.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var srcDoc = PresentationDocument.Open(templatePath, isEditable: false);
|
using var srcDoc = PresentationDocument.Open(templatePath, isEditable: false);
|
||||||
var srcPresPart = srcDoc.PresentationPart;
|
var srcPresPart = srcDoc.PresentationPart;
|
||||||
if (srcPresPart == null) return false;
|
if (srcPresPart == null)
|
||||||
|
return TemplateMasterCloneResult.Failed("missing_presentation_part", "The source PPTX has no presentation part.");
|
||||||
|
|
||||||
var srcMaster = srcPresPart.SlideMasterParts.FirstOrDefault();
|
var srcMaster = srcPresPart.SlideMasterParts.FirstOrDefault();
|
||||||
if (srcMaster == null) return false;
|
if (srcMaster == null)
|
||||||
|
return TemplateMasterCloneResult.Failed("missing_slide_master", "The source PPTX has no slide master.");
|
||||||
|
|
||||||
// 1. SlideMasterPart 통째로 복제 (ThemePart, 이미지, 레이아웃 포함)
|
// 1. SlideMasterPart 통째로 복제 (ThemePart, 이미지, 레이아웃 포함)
|
||||||
var clonedMaster = targetPresPart.AddPart(srcMaster);
|
var clonedMaster = targetPresPart.AddPart(srcMaster);
|
||||||
@@ -2545,20 +2680,71 @@ public class PptxSkill : IAgentTool
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layoutPart = blankLayout ?? firstLayout;
|
|
||||||
if (layoutPart == null) return false;
|
var layoutPart = blankLayout ?? firstLayout;
|
||||||
|
if (layoutPart == null)
|
||||||
|
return TemplateMasterCloneResult.Failed("missing_slide_layout", "The source PPTX has no reusable slide layout.");
|
||||||
|
|
||||||
// 4. 마스터 저장
|
// 4. 마스터 저장
|
||||||
clonedMaster.SlideMaster.Save();
|
clonedMaster.SlideMaster.Save();
|
||||||
return true;
|
return TemplateMasterCloneResult.Succeeded(layoutPart);
|
||||||
|
}
|
||||||
|
catch (OpenXmlPackageException ex)
|
||||||
|
{
|
||||||
|
Services.LogService.Warn($"템플릿 마스터 복제 실패 ({Path.GetFileName(templatePath)}): {ex.Message}");
|
||||||
|
return TemplateMasterCloneResult.Failed("openxml_package_error", ex.Message);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Services.LogService.Warn($"템플릿 마스터 복제 실패 ({Path.GetFileName(templatePath)}): {ex.Message}");
|
Services.LogService.Warn($"템플릿 마스터 복제 실패 ({Path.GetFileName(templatePath)}): {ex.Message}");
|
||||||
return false;
|
return TemplateMasterCloneResult.Failed("clone_exception", ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatTemplateFallbackLabel(TemplateMasterCloneResult? cloneResult)
|
||||||
|
{
|
||||||
|
if (cloneResult == null || cloneResult.Success)
|
||||||
|
return "color fallback";
|
||||||
|
|
||||||
|
return $"master clone failed:{cloneResult.Status} -> color fallback";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> BuildTemplateDiagnosticsLines(
|
||||||
|
PptxTemplateResolution? explicitTemplateResolution,
|
||||||
|
PptxTemplateResolution? packTemplateResolution,
|
||||||
|
TemplateMasterCloneResult? cloneResult,
|
||||||
|
string? templateName,
|
||||||
|
string? packTemplateName,
|
||||||
|
string? themeFileLabel)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
if (explicitTemplateResolution != null && explicitTemplateResolution.Status == "asset_missing")
|
||||||
|
{
|
||||||
|
lines.Add(
|
||||||
|
$"Template diagnostics: '{explicitTemplateResolution.RequestedName}' asset not found. " +
|
||||||
|
$"Searched {explicitTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
|
||||||
|
}
|
||||||
|
else if (packTemplateResolution != null && packTemplateResolution.Status == "asset_missing")
|
||||||
|
{
|
||||||
|
lines.Add(
|
||||||
|
$"Template diagnostics: pack template '{packTemplateResolution.RequestedName}' asset not found. " +
|
||||||
|
$"Searched {packTemplateResolution.CandidateDirectories.Count} candidate PPT directories.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cloneResult != null && !cloneResult.Success)
|
||||||
|
{
|
||||||
|
var sourceLabel = !string.IsNullOrWhiteSpace(templateName)
|
||||||
|
? $"template '{templateName}'"
|
||||||
|
: !string.IsNullOrWhiteSpace(packTemplateName)
|
||||||
|
? $"pack template '{packTemplateName}'"
|
||||||
|
: $"theme_file '{themeFileLabel}'";
|
||||||
|
lines.Add(
|
||||||
|
$"Template diagnostics: {sourceLabel} master clone failed ({cloneResult.Status}). {cloneResult.Detail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// 슬라이드 페이지 복제 (방법 1 + 방법 2)
|
// 슬라이드 페이지 복제 (방법 1 + 방법 2)
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace AxCopilot.Services.Agent;
|
namespace AxCopilot.Services.Agent;
|
||||||
|
|
||||||
public sealed record PptxTemplateManifestEntry(
|
public sealed class PptxTemplateManifestEntry
|
||||||
string Key,
|
{
|
||||||
string FileName,
|
public string Key { get; init; } = "";
|
||||||
string DisplayName,
|
public string FileName { get; init; } = "";
|
||||||
string FallbackTheme,
|
public string DisplayName { get; init; } = "";
|
||||||
IReadOnlyList<string> Aliases,
|
public string FallbackTheme { get; init; } = "professional";
|
||||||
IReadOnlyList<string> Tags,
|
public List<string> Aliases { get; init; } = [];
|
||||||
IReadOnlyList<string> PackHints);
|
public List<string> Tags { get; init; } = [];
|
||||||
|
public List<string> PackHints { get; init; } = [];
|
||||||
|
public string Tone { get; init; } = "balanced";
|
||||||
|
public string Density { get; init; } = "balanced";
|
||||||
|
public string AspectHint { get; init; } = "widescreen";
|
||||||
|
public string FidelityTier { get; init; } = "standard";
|
||||||
|
public bool SupportsMasterClone { get; init; } = true;
|
||||||
|
public List<string> ObjectiveKeywords { get; init; } = [];
|
||||||
|
public List<string> AudienceKeywords { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PptxTemplateManifest
|
public sealed class PptxTemplateManifest
|
||||||
{
|
{
|
||||||
public List<PptxTemplateManifestEntry> Templates { get; init; } = new();
|
public List<PptxTemplateManifestEntry> Templates { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PptxTemplateResolution(
|
public sealed record PptxTemplateResolution(
|
||||||
@@ -27,6 +38,11 @@ public sealed record PptxTemplateResolution(
|
|||||||
public bool IsResolved => !string.IsNullOrWhiteSpace(ResolvedPath);
|
public bool IsResolved => !string.IsNullOrWhiteSpace(ResolvedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record PptxTemplateRecommendation(
|
||||||
|
PptxTemplateManifestEntry Entry,
|
||||||
|
int Score,
|
||||||
|
string Reason);
|
||||||
|
|
||||||
public static class PptxTemplateManifestCatalog
|
public static class PptxTemplateManifestCatalog
|
||||||
{
|
{
|
||||||
private const string ManifestFileName = "templates.manifest.json";
|
private const string ManifestFileName = "templates.manifest.json";
|
||||||
@@ -54,6 +70,82 @@ public static class PptxTemplateManifestCatalog
|
|||||||
entry.Aliases.Any(alias => string.Equals(alias, normalized, StringComparison.OrdinalIgnoreCase)));
|
entry.Aliases.Any(alias => string.Equals(alias, normalized, StringComparison.OrdinalIgnoreCase)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PptxTemplateRecommendation? RecommendTemplate(string? packName, string? objective, string? audience)
|
||||||
|
{
|
||||||
|
var normalizedPack = packName?.Trim();
|
||||||
|
var normalizedObjective = NormalizeText(objective);
|
||||||
|
var normalizedAudience = NormalizeText(audience);
|
||||||
|
var combinedText = $"{normalizedObjective} {normalizedAudience}".Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedPack) &&
|
||||||
|
string.IsNullOrWhiteSpace(normalizedObjective) &&
|
||||||
|
string.IsNullOrWhiteSpace(normalizedAudience))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestScore = 0;
|
||||||
|
PptxTemplateRecommendation? bestRecommendation = null;
|
||||||
|
foreach (var entry in LoadManifest().Templates)
|
||||||
|
{
|
||||||
|
var score = 0;
|
||||||
|
var reasons = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedPack) &&
|
||||||
|
entry.PackHints.Any(hint => string.Equals(hint, normalizedPack, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
score += 40;
|
||||||
|
reasons.Add($"pack:{normalizedPack}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var objectiveMatches = CountMatches(normalizedObjective, entry.ObjectiveKeywords);
|
||||||
|
if (objectiveMatches > 0)
|
||||||
|
{
|
||||||
|
score += objectiveMatches * 10;
|
||||||
|
reasons.Add($"objective:{objectiveMatches}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var audienceMatches = CountMatches(normalizedAudience, entry.AudienceKeywords);
|
||||||
|
if (audienceMatches > 0)
|
||||||
|
{
|
||||||
|
score += audienceMatches * 8;
|
||||||
|
reasons.Add($"audience:{audienceMatches}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagMatches = CountMatches(combinedText, entry.Tags);
|
||||||
|
if (tagMatches > 0)
|
||||||
|
{
|
||||||
|
score += tagMatches * 4;
|
||||||
|
reasons.Add($"tags:{tagMatches}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.SupportsMasterClone)
|
||||||
|
{
|
||||||
|
score += 3;
|
||||||
|
reasons.Add("master-clone");
|
||||||
|
}
|
||||||
|
|
||||||
|
score += entry.FidelityTier.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"premium" => 6,
|
||||||
|
"high" => 4,
|
||||||
|
"standard" => 2,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (score <= bestScore)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bestScore = score;
|
||||||
|
bestRecommendation = new PptxTemplateRecommendation(
|
||||||
|
entry,
|
||||||
|
score,
|
||||||
|
string.Join(", ", reasons));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestRecommendation;
|
||||||
|
}
|
||||||
|
|
||||||
public static PptxTemplateResolution ResolveTemplate(string? requestedName)
|
public static PptxTemplateResolution ResolveTemplate(string? requestedName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(requestedName))
|
if (string.IsNullOrWhiteSpace(requestedName))
|
||||||
@@ -68,7 +160,7 @@ public static class PptxTemplateManifestCatalog
|
|||||||
null,
|
null,
|
||||||
explicitPath,
|
explicitPath,
|
||||||
explicitPath != null ? "explicit_path" : "explicit_path_missing",
|
explicitPath != null ? "explicit_path" : "explicit_path_missing",
|
||||||
explicitPath != null ? new[] { Path.GetDirectoryName(explicitPath)! } : Array.Empty<string>());
|
explicitPath != null ? [Path.GetDirectoryName(explicitPath)!] : Array.Empty<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry = Resolve(trimmed);
|
var entry = Resolve(trimmed);
|
||||||
@@ -224,6 +316,17 @@ public static class PptxTemplateManifestCatalog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeText(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? "" : value.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
private static int CountMatches(string normalizedText, IEnumerable<string> keywords)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedText))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return keywords.Count(keyword => normalizedText.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
private static bool LooksLikePath(string value)
|
private static bool LooksLikePath(string value)
|
||||||
=> value.Contains('\\', StringComparison.Ordinal)
|
=> value.Contains('\\', StringComparison.Ordinal)
|
||||||
|| value.Contains('/', StringComparison.Ordinal)
|
|| value.Contains('/', StringComparison.Ordinal)
|
||||||
@@ -248,14 +351,142 @@ public static class PptxTemplateManifestCatalog
|
|||||||
{
|
{
|
||||||
Templates =
|
Templates =
|
||||||
[
|
[
|
||||||
new("basic100", "BASIC100 기준 템플릿 V1.pptx", "BASIC100 기준 템플릿 V1", "professional", ["basic", "consulting_basic"], ["executive", "strategy", "consulting", "brand"], ["strategy", "pmo", "sales", "operating_model"]),
|
new PptxTemplateManifestEntry
|
||||||
new("core100", "CORE100 기준템플릿 V1.pptx", "CORE100 기준 템플릿 V1", "corporate", ["core", "board_core"], ["board", "governance", "finance", "corporate"], ["board", "finance"]),
|
{
|
||||||
new("frame_blue", "P01_01_프레임디자인_PPT템플릿_블루(PPT_편집용).pptx", "프레임디자인 블루", "modern", ["blue_frame"], ["frame", "blue", "report", "proposal"], ["sales", "operating_model"]),
|
Key = "basic100",
|
||||||
new("mr_ppt_01", "미스터 피피티 템플릿 01_원본.pptx", "미스터 피피티 템플릿 01", "modern", ["mrppt01"], ["creative", "proposal", "sales"], ["sales"]),
|
FileName = "BASIC100 \uae30\uc900 \ud15c\ud50c\ub9bf V1.pptx",
|
||||||
new("mr_ppt_02", "미스터 피피티 템플릿 02_원본.pptx", "미스터 피피티 템플릿 02", "vibrant", ["mrppt02"], ["creative", "marketing", "proposal"], ["sales"]),
|
DisplayName = "BASIC100 template",
|
||||||
new("mr_ppt_03", "미스터 피피티 03_원본.pptx", "미스터 피피티 템플릿 03", "minimal", ["mrppt03"], ["minimal", "analysis", "report"], ["strategy", "finance"]),
|
FallbackTheme = "professional",
|
||||||
new("mr_ppt_04", "미스터 피피티 템플릿 04_원본.pptx", "미스터 피피티 템플릿 04", "slate", ["mrppt04"], ["dark", "executive", "review"], ["board", "finance"]),
|
Aliases = ["basic", "consulting_basic"],
|
||||||
new("mr_ppt_05", "미스터 피피티 템플릿_05_원본.pptx", "미스터 피피티 템플릿 05", "ocean", ["mrppt05"], ["blue", "modern", "report"], ["pmo", "operating_model"]),
|
Tags = ["executive", "strategy", "consulting", "brand"],
|
||||||
|
PackHints = ["strategy", "pmo", "sales", "operating_model"],
|
||||||
|
Tone = "consulting",
|
||||||
|
Density = "balanced",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "premium",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["strategy", "transformation", "operating model", "growth", "roadmap"],
|
||||||
|
AudienceKeywords = ["executive", "leadership", "steering", "pmo", "business"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "core100",
|
||||||
|
FileName = "CORE100 \uae30\uc900\ud15c\ud50c\ub9bf V1.pptx",
|
||||||
|
DisplayName = "CORE100 template",
|
||||||
|
FallbackTheme = "corporate",
|
||||||
|
Aliases = ["core", "board_core"],
|
||||||
|
Tags = ["board", "governance", "finance", "corporate"],
|
||||||
|
PackHints = ["board", "finance"],
|
||||||
|
Tone = "corporate",
|
||||||
|
Density = "concise",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "premium",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["board", "governance", "finance", "budget", "investment"],
|
||||||
|
AudienceKeywords = ["board", "executive", "committee", "cfo", "directors"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "frame_blue",
|
||||||
|
FileName = "P01_01_\ud504\ub808\uc784\ub514\uc790\uc778_PPT\ud15c\ud50c\ub9bf_\ube14\ub8e8(PPT_\ud3b8\uc9d1\uc6a9).pptx",
|
||||||
|
DisplayName = "Blue frame template",
|
||||||
|
FallbackTheme = "modern",
|
||||||
|
Aliases = ["blue_frame"],
|
||||||
|
Tags = ["frame", "blue", "report", "proposal"],
|
||||||
|
PackHints = ["sales", "operating_model"],
|
||||||
|
Tone = "proposal",
|
||||||
|
Density = "balanced",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "high",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["proposal", "operating model", "sales", "status", "report"],
|
||||||
|
AudienceKeywords = ["client", "sales", "project", "leadership"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "mr_ppt_01",
|
||||||
|
FileName = "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 01_\uc6d0\ubcf8.pptx",
|
||||||
|
DisplayName = "Mr PPT 01",
|
||||||
|
FallbackTheme = "modern",
|
||||||
|
Aliases = ["mrppt01"],
|
||||||
|
Tags = ["creative", "proposal", "sales"],
|
||||||
|
PackHints = ["sales"],
|
||||||
|
Tone = "creative",
|
||||||
|
Density = "balanced",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "standard",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["proposal", "pitch", "sales", "commercial"],
|
||||||
|
AudienceKeywords = ["client", "sales", "commercial"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "mr_ppt_02",
|
||||||
|
FileName = "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 02_\uc6d0\ubcf8.pptx",
|
||||||
|
DisplayName = "Mr PPT 02",
|
||||||
|
FallbackTheme = "vibrant",
|
||||||
|
Aliases = ["mrppt02"],
|
||||||
|
Tags = ["creative", "marketing", "proposal"],
|
||||||
|
PackHints = ["sales"],
|
||||||
|
Tone = "bold",
|
||||||
|
Density = "condensed",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "standard",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["marketing", "launch", "brand", "proposal"],
|
||||||
|
AudienceKeywords = ["marketing", "sales", "brand"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "mr_ppt_03",
|
||||||
|
FileName = "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 03_\uc6d0\ubcf8.pptx",
|
||||||
|
DisplayName = "Mr PPT 03",
|
||||||
|
FallbackTheme = "minimal",
|
||||||
|
Aliases = ["mrppt03"],
|
||||||
|
Tags = ["minimal", "analysis", "report"],
|
||||||
|
PackHints = ["strategy", "finance"],
|
||||||
|
Tone = "analytical",
|
||||||
|
Density = "concise",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "high",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["analysis", "review", "report", "diagnostic"],
|
||||||
|
AudienceKeywords = ["strategy", "finance", "leadership"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "mr_ppt_04",
|
||||||
|
FileName = "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf 04_\uc6d0\ubcf8.pptx",
|
||||||
|
DisplayName = "Mr PPT 04",
|
||||||
|
FallbackTheme = "slate",
|
||||||
|
Aliases = ["mrppt04"],
|
||||||
|
Tags = ["dark", "executive", "review"],
|
||||||
|
PackHints = ["board", "finance"],
|
||||||
|
Tone = "executive",
|
||||||
|
Density = "condensed",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "high",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["board review", "risk", "finance", "committee"],
|
||||||
|
AudienceKeywords = ["board", "executive", "cfo"]
|
||||||
|
},
|
||||||
|
new PptxTemplateManifestEntry
|
||||||
|
{
|
||||||
|
Key = "mr_ppt_05",
|
||||||
|
FileName = "\ubbf8\uc2a4\ud130 \ud53c\ud53c\ud2f0 \ud15c\ud50c\ub9bf_05_\uc6d0\ubcf8.pptx",
|
||||||
|
DisplayName = "Mr PPT 05",
|
||||||
|
FallbackTheme = "ocean",
|
||||||
|
Aliases = ["mrppt05"],
|
||||||
|
Tags = ["blue", "modern", "report"],
|
||||||
|
PackHints = ["pmo", "operating_model"],
|
||||||
|
Tone = "operational",
|
||||||
|
Density = "balanced",
|
||||||
|
AspectHint = "widescreen",
|
||||||
|
FidelityTier = "high",
|
||||||
|
SupportsMasterClone = true,
|
||||||
|
ObjectiveKeywords = ["pmo", "roadmap", "status", "operating model"],
|
||||||
|
AudienceKeywords = ["pmo", "operations", "delivery", "steering"]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user