153 lines
5.5 KiB
Python

"""作者明确圈选的正文及保护范围;通用改写不制造机器味诊断。"""
from dataclasses import asdict
from typing import Literal
from pydantic import ConfigDict, StrictInt
from pydantic.dataclasses import dataclass
from muse.作品规划.接口 import 读取章节归属
from muse.正式变更.接口 import 固定哈希
from muse.正文写作.存储 import 正文存储
from muse.正文写作.模型 import 正文错误
from muse.正文写作.正文格式 import 文稿身份
from muse.正文写作.选段编辑 import 应用选段, 段落文本, 选段修改
@dataclass(frozen=True, config=ConfigDict(extra="forbid"))
class 原文选段:
paragraph_id: str
paragraph_hash: str
start: StrictInt
end: StrictInt
original: str
def __post_init__(self):
if not 0 <= self.start < self.end or not self.original:
raise 正文错误("圈选需要非空原文和明确码点范围")
self.重放项()
def 重放项(self):
return 选段修改(
self.paragraph_id,
self.paragraph_hash,
self.start,
self.end,
self.original,
self.original,
)
@property
def selection_id(self):
return 固定哈希(asdict(self))
@dataclass(frozen=True, config=ConfigDict(extra="forbid"))
class 选区坐标:
paragraph_id: str
start: StrictInt
end: StrictInt
original: str
def 定位原文选区(连, 作者, work_id, chapter_id, branch_id, revision, document_hash, 坐标):
from muse.正文写作.选段编辑 import 段落哈希
chapter = 读取章节归属(连, 作者, chapter_id)
row = 正文存储(连).读取(文稿身份(chapter_id, branch_id))
if (
chapter["work_id"] != work_id
or row is None
or (row["revision"], row["document_hash"]) != (revision, document_hash)
):
raise 正文错误("圈选正文版本已变化")
doc = 正文存储.恢复草稿(row)
if not 1 <= len(坐标) <= 32:
raise 正文错误("需要1至32个确切选段")
by_id = {p.paragraph_id: p for p in doc.paragraphs}
out = []
for s in 坐标:
if s.paragraph_id not in by_id:
raise 正文错误("圈选段落不存在")
out.append(
原文选段(s.paragraph_id, 段落哈希(by_id[s.paragraph_id]), s.start, s.end, s.original)
)
应用选段(doc, tuple(s.重放项() for s in out))
return out
@dataclass(frozen=True, config=ConfigDict(extra="forbid"))
class 选段写作授权:
work_id: str
chapter_id: str
branch_id: str
revision: StrictInt
document_hash: str
mode: Literal["expand", "compress", "polish", "rewrite"]
purpose: str
selections: tuple[原文选段, ...]
protected_ranges: tuple[原文选段, ...] = ()
references: tuple[str, ...] = ()
def __post_init__(self):
if self.revision < 1 or not self.purpose.strip() or not 1 <= len(self.selections) <= 32:
raise 正文错误("选段改写需已保存版本、目的和1至32个选段")
if not self.work_id or not self.chapter_id or not self.branch_id:
raise 正文错误("选段改写需明确作品、章节及分支")
if len({s.selection_id for s in self.selections}) != len(self.selections):
raise 正文错误("圈选不能重复")
if len(self.references) > 8:
raise 正文错误("参考方法不超过8个")
if len(set(self.references)) != len(self.references):
raise 正文错误("参考方法版本不能重复")
if not all(type(r) is str and r.strip() for r in self.references):
raise 正文错误("参考方法需要非空版本身份")
def 准备选段改写(连, 作者, 授权: 选段写作授权):
chapter = 读取章节归属(连, 作者, 授权.chapter_id)
store = 正文存储(连)
row = store.读取(文稿身份(授权.chapter_id, 授权.branch_id))
if (
chapter["work_id"] != 授权.work_id
or row is None
or (row["revision"], row["document_hash"]) != (授权.revision, 授权.document_hash)
):
raise 正文错误("圈选的正文版本已改变或不属于该作品")
doc = store.恢复草稿(row)
patches = tuple(s.重放项() for s in 授权.selections)
应用选段(doc, patches)
应用选段(doc, tuple(s.重放项() for s in 授权.protected_ranges))
for s in 授权.selections:
for p in 授权.protected_ranges:
if s.paragraph_id == p.paragraph_id and max(s.start, p.start) < min(s.end, p.end):
raise 正文错误("自动改写范围与作者保护区重叠")
slots = []
for s in 授权.selections:
paragraph = next(p for p in doc.paragraphs if p.paragraph_id == s.paragraph_id)
text = 段落文本(paragraph)
slots.append(
{
"selection_id": s.selection_id,
"original": s.original,
"context": text[max(0, s.start - 160) : s.end + 160],
}
)
return (
{
"work_id": 授权.work_id,
"chapter_id": 授权.chapter_id,
"branch_id": 授权.branch_id,
"revision": row["revision"],
"document_hash": row["document_hash"],
"document": doc,
},
patches,
{
"mode": 授权.mode,
"purpose": 授权.purpose,
"slots": slots,
"protected_ranges": [asdict(p) for p in 授权.protected_ranges],
},
)