Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/python-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: python-quality

on:
pull_request:
push:
branches:
- main

jobs:
quality:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Setup uv
uses: astral-sh/setup-uv@v5

- name: Check formatting (ruff)
run: uvx ruff format --check skills/distill-knowledge/scripts

- name: Lint (ruff)
run: uvx ruff check skills/distill-knowledge/scripts

- name: Syntax check (compileall)
run: python -m compileall -q skills/distill-knowledge/scripts

- name: Run tests
run: uvx pytest
16 changes: 16 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.13
hooks:
- id: ruff-format
files: ^skills/distill-knowledge/scripts/.*\.py$
- id: ruff-check
files: ^skills/distill-knowledge/scripts/.*\.py$

- repo: local
hooks:
- id: python-compileall
name: python compileall (syntax check)
entry: python3 -m compileall -q skills/distill-knowledge/scripts
language: system
pass_filenames: false
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.PHONY: format lint syntax test quality install-hooks

PY_SCRIPTS=skills/distill-knowledge/scripts

format:
uvx ruff format $(PY_SCRIPTS)

lint:
uvx ruff check $(PY_SCRIPTS)

syntax:
python3 -m compileall -q $(PY_SCRIPTS)

test:
uvx pytest

quality:
uvx ruff format --check $(PY_SCRIPTS)
uvx ruff check $(PY_SCRIPTS)
python3 -m compileall -q $(PY_SCRIPTS)
uvx pytest

install-hooks:
uv tool install pre-commit
pre-commit install
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ The published GitHub repo is named `distill-knowledge` to match the skill (skill

The skill picks the path at Gate 1 of the workflow and asks you to confirm before spending API budget.

## Development checks

```bash
make quality # format check + lint + syntax + tests
make format # apply formatter
make install-hooks # enable commit-time checks
```

CI runs the same quality checks in `.github/workflows/python-quality.yml`.

## License

MIT — see [LICENSE](LICENSE).
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[tool.ruff]
target-version = "py310"
line-length = 100
extend-exclude = [
"tmp",
"inbox",
"outbox",
]

[tool.ruff.lint]
select = ["E", "F"]

[tool.pytest.ini_options]
testpaths = ["skills/distill-knowledge/scripts/tests"]
pythonpath = ["skills/distill-knowledge/scripts"]
addopts = "-q"
2 changes: 1 addition & 1 deletion skills/distill-knowledge/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ compatibility: Requires uv, ffmpeg, ffprobe, Python ≥3.10, and OPENAI_API_KEY
license: MIT
metadata:
author: Dim Kharitonov <dimds@fastmail.com> (https://github.com/dimdasci)
version: "1.0.0"
version: "1.1.0"
---

# Convert Recording → Knowledge Markdown
Expand Down
49 changes: 35 additions & 14 deletions skills/distill-knowledge/scripts/extract_clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
--duration 6
--out tmp/clips/clip_<seconds>.ogg
"""

from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
Expand All @@ -50,15 +50,22 @@ def parse_timestamp(value: str) -> float:


def main() -> int:
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
p.add_argument("audio", type=Path, help="Source audio or video file")
p.add_argument("--at", type=parse_timestamp, required=True,
help="Start timestamp (seconds, MM:SS, or H:MM:SS)")
p.add_argument("--duration", type=float, default=6.0,
help="Clip duration in seconds (default 6)")
p.add_argument("--out", type=Path, default=None,
help="Output path (default tmp/clips/clip_<seconds>.ogg)")
p.add_argument(
"--at",
type=parse_timestamp,
required=True,
help="Start timestamp (seconds, MM:SS, or H:MM:SS)",
)
p.add_argument(
"--duration", type=float, default=6.0, help="Clip duration in seconds (default 6)"
)
p.add_argument(
"--out", type=Path, default=None, help="Output path (default tmp/clips/clip_<seconds>.ogg)"
)
args = p.parse_args()

if not shutil.which("ffmpeg"):
Expand All @@ -75,12 +82,26 @@ def main() -> int:
out.parent.mkdir(parents=True, exist_ok=True)

cmd = [
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
"-ss", f"{args.at}",
"-t", f"{args.duration}",
"-i", str(args.audio),
"ffmpeg",
"-y",
"-hide_banner",
"-loglevel",
"error",
"-ss",
f"{args.at}",
"-t",
f"{args.duration}",
"-i",
str(args.audio),
"-vn",
"-c:a", "libopus", "-b:a", "32k", "-ac", "1", "-ar", "16000",
"-c:a",
"libopus",
"-b:a",
"32k",
"-ac",
"1",
"-ar",
"16000",
str(out),
]
res = subprocess.run(cmd, capture_output=True, text=True)
Expand Down
67 changes: 39 additions & 28 deletions skills/distill-knowledge/scripts/merge_chunks.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,15 @@ def _annotate_segments(
elif abs_end > core_end and chunk_end > core_end:
in_overlap = "next"

annotated.append({
**seg,
"start": abs_start,
"end": abs_end,
"source_chunk": chunk_index,
"in_overlap": in_overlap,
})
annotated.append(
{
**seg,
"start": abs_start,
"end": abs_end,
"source_chunk": chunk_index,
"in_overlap": in_overlap,
}
)
return annotated


Expand All @@ -131,7 +133,6 @@ def _build_overlap_windows(

for i in range(len(chunks) - 1):
left_chunk = chunks[i]
right_chunk = chunks[i + 1]

# Shared region: from core_end - overlap to core_end + overlap
# More precisely: the region where both chunks have data
Expand All @@ -141,22 +142,26 @@ def _build_overlap_windows(

# Left segments in shared region (timestamps are now absolute)
left_segs = [
seg for seg in all_annotated[i]
seg
for seg in all_annotated[i]
if seg["end"] > shared_start and seg["start"] < shared_end
]

# Right segments in shared region
right_segs = [
seg for seg in all_annotated[i + 1]
seg
for seg in all_annotated[i + 1]
if seg["end"] > shared_start and seg["start"] < shared_end
]

windows.append({
"boundary_idx": i,
"shared_region": [round(shared_start, 1), round(shared_end, 1)],
"left": left_segs,
"right": right_segs,
})
windows.append(
{
"boundary_idx": i,
"shared_region": [round(shared_start, 1), round(shared_end, 1)],
"left": left_segs,
"right": right_segs,
}
)

return windows

Expand All @@ -169,14 +174,16 @@ def _build_chunk_boundaries(manifest: dict) -> list[dict]:

for i in range(len(chunks) - 1):
core_end = chunks[i]["core_end_s"]
boundaries.append({
"index": i,
"core_end_s": core_end,
"shared_region": [
round(core_end - overlap_s, 1),
round(core_end + overlap_s, 1),
],
})
boundaries.append(
{
"index": i,
"core_end_s": core_end,
"shared_region": [
round(core_end - overlap_s, 1),
round(core_end + overlap_s, 1),
],
}
)

return boundaries

Expand All @@ -199,19 +206,23 @@ def main() -> None:
description="Merge per-chunk transcription JSONs into unified timeline."
)
parser.add_argument(
"--manifest", required=True,
"--manifest",
required=True,
help="Path to manifest.json from prep_audio.py",
)
parser.add_argument(
"--intake", required=True,
"--intake",
required=True,
help="JSON string with intake context (speaker_count, speaker_names, topic, terms)",
)
parser.add_argument(
"--vtt", default=None,
"--vtt",
default=None,
help="Path to VTT file for cross-check cues (optional)",
)
parser.add_argument(
"--out", required=True,
"--out",
required=True,
help="Output path for merged.json",
)

Expand Down
Loading
Loading