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>
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""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
|
|
],
|
|
)
|
|
|