style.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { Command } from "commander";
  2. import { StateManager, analyzeStyle, PipelineRunner } from "@actalk/inkos-core";
  3. import { loadConfig, buildPipelineConfig, findProjectRoot, resolveBookId, log, logError } from "../utils.js";
  4. import { readFile, writeFile, mkdir } from "node:fs/promises";
  5. import { join, resolve } from "node:path";
  6. export const styleCommand = new Command("style")
  7. .description("Style fingerprint analysis and import");
  8. styleCommand
  9. .command("analyze")
  10. .description("Analyze a text file and extract style profile")
  11. .argument("<file>", "Text file to analyze")
  12. .option("--name <name>", "Source name for the profile")
  13. .option("--json", "Output JSON only")
  14. .action(async (file: string, opts) => {
  15. try {
  16. const text = await readFile(resolve(file), "utf-8");
  17. const profile = analyzeStyle(text, opts.name ?? file);
  18. if (opts.json) {
  19. log(JSON.stringify(profile, null, 2));
  20. } else {
  21. log("Style Profile:");
  22. log(` Source: ${profile.sourceName ?? "unknown"}`);
  23. log(` Avg sentence length: ${profile.avgSentenceLength} chars`);
  24. log(` Sentence length std dev: ${profile.sentenceLengthStdDev}`);
  25. log(` Avg paragraph length: ${profile.avgParagraphLength} chars`);
  26. log(` Paragraph range: ${profile.paragraphLengthRange.min}-${profile.paragraphLengthRange.max} chars`);
  27. log(` Vocabulary diversity (TTR): ${profile.vocabularyDiversity}`);
  28. if (profile.topPatterns.length > 0) {
  29. log(` Top patterns: ${profile.topPatterns.join(", ")}`);
  30. }
  31. if (profile.rhetoricalFeatures.length > 0) {
  32. log(` Rhetorical features: ${profile.rhetoricalFeatures.join(", ")}`);
  33. }
  34. }
  35. } catch (e) {
  36. logError(`Analysis failed: ${e}`);
  37. process.exit(1);
  38. }
  39. });
  40. styleCommand
  41. .command("import")
  42. .description("Import style profile + generate style guide (LLM) into a book")
  43. .argument("<file>", "Text file to analyze and import")
  44. .argument("[book-id]", "Book ID (auto-detected if only one book)")
  45. .option("--name <name>", "Source name for the profile")
  46. .option("--stats-only", "Only save statistical profile, skip LLM style guide generation")
  47. .option("--json", "Output JSON")
  48. .action(async (file: string, bookIdArg: string | undefined, opts) => {
  49. try {
  50. const root = findProjectRoot();
  51. const bookId = await resolveBookId(bookIdArg, root);
  52. const state = new StateManager(root);
  53. const bookDir = state.bookDir(bookId);
  54. const text = await readFile(resolve(file), "utf-8");
  55. const profile = analyzeStyle(text, opts.name ?? file);
  56. const storyDir = join(bookDir, "story");
  57. await mkdir(storyDir, { recursive: true });
  58. await writeFile(
  59. join(storyDir, "style_profile.json"),
  60. JSON.stringify(profile, null, 2),
  61. "utf-8",
  62. );
  63. if (!opts.json) log(`Statistical profile saved (TTR: ${profile.vocabularyDiversity})`);
  64. // LLM-powered style guide generation
  65. if (!opts.statsOnly) {
  66. if (!opts.json) log("Generating qualitative style guide via LLM...");
  67. const config = await loadConfig();
  68. const pipeline = new PipelineRunner(buildPipelineConfig(config, root));
  69. await pipeline.generateStyleGuide(bookId, text, opts.name ?? file);
  70. if (!opts.json) log("Style guide (style_guide.md) generated.");
  71. }
  72. if (opts.json) {
  73. log(JSON.stringify({
  74. bookId,
  75. file,
  76. statsProfile: `story/style_profile.json`,
  77. styleGuide: opts.statsOnly ? null : `story/style_guide.md`,
  78. }, null, 2));
  79. } else {
  80. log(`Style imported to "${bookId}" from "${file}"`);
  81. }
  82. } catch (e) {
  83. if (opts.json) {
  84. log(JSON.stringify({ error: String(e) }));
  85. } else {
  86. logError(`Import failed: ${e}`);
  87. }
  88. process.exit(1);
  89. }
  90. });