refactor: _shared.py vira subpacote, um módulo por papel
Eram 882 linhas de seis papéis sem relação, sob um nome que só dizia
"compartilhado" — o depósito onde tudo que servia a mais de um handler
acabava caindo.
media 316 transcrição em cache, corte por fala, relatório
paths 206 sandbox, limites, caminho de saída
project 116 abrir projeto, preparar modifier/generator
captions 112 SRT, VTT, listas com timestamp
detection 99 flash frames, buracos, duplicados
formatting 86 tabelas e relatórios dos handlers
O __init__ reexporta os 46 nomes, então os treze pontos que importam daqui
não mudaram.
_transcript_cut_report saiu de formatting para media: ele precisa do hint de
instalação e do _text_result, ou seja, é relatório de transcrição e não
formatação genérica — mover foi mais honesto que cruzar imports entre os
dois módulos.
Quatro testes patchavam `server_tools._shared.transcribe`; o nome agora é
ligado por _shared/media.py, então o patch passou a apontar para lá — mesmo
padrão da experiência #23.
Lint zerado, 1454 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
368bb62706
commit
ffaebb3f72
@@ -0,0 +1,114 @@
|
||||
"""Formatação do texto que os handlers devolvem — tabelas e relatórios.
|
||||
|
||||
Extraído de _shared.py — ver server_tools/_shared/__init__.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
def format_timecode(tc) -> str:
|
||||
"""Format a Timecode object to SMPTE string."""
|
||||
return tc.to_smpte() if tc else "00:00:00:00"
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""Format seconds into human-readable duration."""
|
||||
if seconds < 1:
|
||||
return f"{seconds*1000:.0f}ms"
|
||||
elif seconds < 60:
|
||||
return f"{seconds:.2f}s"
|
||||
return f"{int(seconds // 60)}m {seconds % 60:.1f}s"
|
||||
|
||||
def _format_clip_table(clips: list, header: str) -> str:
|
||||
"""Render a list of clips as a markdown table with timecodes and durations.
|
||||
|
||||
Shared by handlers that filter clips by duration threshold
|
||||
(find_short_cuts, find_long_clips).
|
||||
"""
|
||||
result = f"{header}\n\n| Name | TC | Duration |\n|------|----|---------|\n"
|
||||
result += "\n".join(
|
||||
f"| {c.name} | {format_timecode(c.start)} | {format_duration(c.duration_seconds)} |"
|
||||
for c in clips
|
||||
)
|
||||
return result
|
||||
|
||||
def _markdown_table(headers: list[str], rows: list[list[str]]) -> str:
|
||||
"""Build a markdown table from headers and rows.
|
||||
|
||||
Returns header row, separator row, and data rows as a single string.
|
||||
Callers avoid repeating the ``| H1 | H2 |\\n|---|---|`` boilerplate
|
||||
that appears in 15+ handlers.
|
||||
"""
|
||||
header_line = "| " + " | ".join(headers) + " |"
|
||||
sep_line = "|" + "|".join("------" for _ in headers) + "|"
|
||||
data_lines = "\n".join(
|
||||
"| " + " | ".join(str(c) for c in row) + " |" for row in rows
|
||||
)
|
||||
return f"{header_line}\n{sep_line}\n{data_lines}"
|
||||
|
||||
def _format_batch_result(
|
||||
title: str,
|
||||
summary: dict[str, str],
|
||||
headers: list[str],
|
||||
rows: list[list[str]],
|
||||
output_path: str,
|
||||
) -> str:
|
||||
"""Build a standard batch-operation result with summary, table, and save footer.
|
||||
|
||||
Used by batch fix handlers (flash frames, rapid trim, fill gaps) that all
|
||||
share the same markdown structure: ``# Title → ## Summary → ## Details table
|
||||
→ Saved to`` footer.
|
||||
"""
|
||||
summary_lines = "\n".join(f"- **{k}**: {v}" for k, v in summary.items())
|
||||
table = _markdown_table(headers, rows)
|
||||
return (
|
||||
f"# {title}\n\n"
|
||||
f"## Summary\n{summary_lines}\n\n"
|
||||
f"## Details\n{table}\n\n"
|
||||
f"Saved to: `{output_path}`"
|
||||
)
|
||||
|
||||
def _fmt_suggestions(suggestions: list[str]) -> str:
|
||||
"""Format pacing suggestions as markdown list (Python 3.10 compatible)."""
|
||||
if not suggestions:
|
||||
return "- Pacing looks good!"
|
||||
nl = "\n"
|
||||
return nl.join(f"- {s}" for s in suggestions)
|
||||
|
||||
def _voice_analysis_config_text(config: dict) -> str:
|
||||
w = config["emphasis_weights"]
|
||||
text = "# Voice Analysis Settings\n\n"
|
||||
text += _markdown_table(
|
||||
["Setting", "Value"],
|
||||
[
|
||||
["Energy threshold", f"{config['energy_threshold']:.2f}"],
|
||||
["Peak selection", f"top {config['peak_percentile']:.1%} of words"],
|
||||
["Emphasis floor", f"{config['emphasis_floor']:.2f}"],
|
||||
["Emotion detection", "on" if config["emotion_enabled"] else "off"],
|
||||
["Emotion sensitivity", f"{config['emotion_sensitivity']:.2f}"],
|
||||
],
|
||||
) + "\n\n## Emphasis Weights\n"
|
||||
text += _markdown_table(
|
||||
["Factor", "Weight"],
|
||||
[[k.replace("_", " ").title(), f"{v:.2f}"] for k, v in w.items()],
|
||||
)
|
||||
return text
|
||||
|
||||
def _speaker_table(profiles: Sequence[dict]) -> str:
|
||||
"""Who was detected, ordered by how much of the runtime each holds."""
|
||||
return _markdown_table(
|
||||
["ID", "Name", "Share", "Speaking", "Lines", "Avg line"],
|
||||
[
|
||||
[
|
||||
p["id"],
|
||||
p.get("name", ""),
|
||||
f"{p['share']:.0%}",
|
||||
format_duration(p["speaking_seconds"]),
|
||||
str(p["segment_count"]),
|
||||
f"{p['avg_segment']:.1f}s",
|
||||
]
|
||||
for p in profiles
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user