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
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ jobs:

- name: Run smoke test
run: pycaps --help

- name: Run unit tests
run: python -m unittest discover -s tests -p "test_*.py"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ demo
TODO.md
research
.secrets
.DS_Store
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ This command will:
3. Apply the template's styles and animations.
4. Save the result in a new file.

If you already have a transcript from another tool, you can skip built-in transcription:

```bash
pycaps render --input my_video.mp4 --template minimalist --transcript transcript.json
```

Supported transcript formats: `whisper_json`, `pycaps_json`, `srt`, `vtt` (`--transcript-format` is optional and defaults to `auto`).

### 2. Using the Python Library

For full control, use the `CapsPipelineBuilder` in your Python code.
Expand Down
25 changes: 24 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,32 @@ This will create a new video named `output_... .mp4` in your current directory.
pycaps render ... --preview-time 10.5,15
```
- `--subtitle-data <path>`: Skips transcription and uses a pre-generated `.json` data file. Great for re-rendering with different styles.
- `--transcript <path>`: Uses an external transcription input and skips built-in STT.
- `--transcript-format <auto|whisper_json|pycaps_json|srt|vtt>`: Declares transcript format (defaults to `auto`).
- `-v`, `--verbose`: Show detailed logs during processing.

### External Transcript Input

If you already transcribed audio with another tool, you can pass that file directly:

```bash
pycaps render --input my_video.mp4 --template minimalist --transcript transcript.json
```

Supported formats:
- `whisper_json` (Whisper response with `segments[].words[]`)
- `pycaps_json` (pycaps document JSON or lightweight `segments[].words[]`)
- `srt`
- `vtt` (including inline timestamp tags)

Use `--transcript-format` when auto-detection is not enough:

```bash
pycaps render --input my_video.mp4 --template minimalist --transcript subtitles.vtt --transcript-format vtt
```

`--subtitle-data` is different: it expects already processed pycaps subtitle data and skips both transcription and processing. `--transcript` still runs processing (splitters, tags, effects) after loading text/timings.

## `pycaps preview-styles`

This command launches a GUI to help you design your subtitle styles in real-time, without needing a video.
Expand Down Expand Up @@ -88,4 +112,3 @@ Manage your Pycaps API key for AI features.
- `pycaps config`: Shows your currently saved API key, if any.
- `pycaps config --set-api-key <your-key>`: Saves your API key locally.
- `pycaps config --unset-api-key`: Removes your saved API key.

29 changes: 28 additions & 1 deletion docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ pipeline.run()
print("Video has been rendered!")
```

---
## Example 2.5: Render Using an Existing Transcript

### CLI

```bash
pycaps render --input my_video.mp4 --template minimalist --transcript transcript.srt
```

### Python

```python
from pycaps import CapsPipelineBuilder, TranscriptFormat

pipeline = (
CapsPipelineBuilder()
.with_input_video("my_video.mp4")
.with_transcription_file("transcript.vtt", TranscriptFormat.VTT)
.add_css("styles.css")
.build()
)

pipeline.run()
```

You can also pass a dict or a `Document` directly with `with_transcription(...)`.

---
## Example 3: Advanced JSON with Tagger and Effects

Expand Down Expand Up @@ -200,4 +227,4 @@ builder.add_animation(
pipeline = builder.build()
pipeline.run()

print("Advanced pipeline finished successfully!")
print("Advanced pipeline finished successfully!")
4 changes: 2 additions & 2 deletions src/pycaps/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .pipeline import CapsPipeline, CapsPipelineBuilder, JsonConfigLoader
from .renderer import CssSubtitleRenderer, PictexSubtitleRenderer
from .transcriber import WhisperAudioTranscriber, GoogleAudioTranscriber, AudioTranscriber, LimitByWordsSplitter, LimitByCharsSplitter, SplitIntoSentencesSplitter
from .transcriber import WhisperAudioTranscriber, GoogleAudioTranscriber, AudioTranscriber, LimitByWordsSplitter, LimitByCharsSplitter, SplitIntoSentencesSplitter, TranscriptFormat, load_transcription
from .effect import *
from .animation import *
from .selector import WordClipSelector
Expand All @@ -10,4 +10,4 @@
from .ai import LlmProvider
from .template import TemplateLoader, TemplateFactory, DEFAULT_TEMPLATE_NAME

__version__ = "0.2.1"
__version__ = "0.2.1"
10 changes: 10 additions & 0 deletions src/pycaps/cli/render_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pycaps.common import VideoQuality
from pycaps.layout import VerticalAlignmentType, SubtitleLayoutOptions
from pycaps.template import TemplateLoader, DEFAULT_TEMPLATE_NAME, TemplateFactory
from pycaps.transcriber import TranscriptFormat

render_app = typer.Typer()

Expand Down Expand Up @@ -62,12 +63,20 @@ def render(
preview: bool = typer.Option(False, "--preview", help="Generate a low quality preview of the rendered video", rich_help_panel="Utils"),
preview_time: Optional[str] = typer.Option(None, "--preview-time", help="Generate a low quality preview of the rendered video at the given time, example: --preview-time=10,15", rich_help_panel="Utils", show_default=False),
subtitle_data: Optional[str] = typer.Option(None, "--subtitle-data", help="Subtitle data file path. If provided, the rendering process will skip the transcription and tagging steps", rich_help_panel="Utils", show_default=False),
transcript: Optional[str] = typer.Option(None, "--transcript", help="Path to an external transcript file. If provided, pycaps skips built-in transcription", rich_help_panel="Utils", show_default=False),
transcript_format: TranscriptFormat = typer.Option(TranscriptFormat.AUTO, "--transcript-format", help="Transcript format: auto|whisper_json|pycaps_json|srt|vtt", rich_help_panel="Utils", show_default=False),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose mode", rich_help_panel="Utils"),
):
set_logging_level(logging.DEBUG if verbose else logging.INFO)
if template_name and config_file:
typer.echo("Only one of --template or --config can be provided", err=True)
return None
if subtitle_data and transcript:
typer.echo("Only one of --subtitle-data or --transcript can be provided", err=True)
return None
if transcript_format != TranscriptFormat.AUTO and not transcript:
typer.echo("--transcript-format requires --transcript", err=True)
return None

if not template_name and not config_file:
template_name = DEFAULT_TEMPLATE_NAME
Expand All @@ -85,6 +94,7 @@ def render(
# TODO: this has a little issue (if you set lang via js + whisper model by cli, it will change the lang to None)
if language or whisper_model or whisper_prompt: builder.with_whisper_config(language=language, model_size=whisper_model if whisper_model else "base", initial_prompt=whisper_prompt)
if subtitle_data: builder.with_subtitle_data_path(subtitle_data)
if transcript: builder.with_transcription_file(transcript, transcript_format)
if transcription_preview: builder.should_preview_transcription(True)
if video_quality: builder.with_video_quality(video_quality)
if layout_align or layout_align_offset: builder.with_layout_options(_build_layout_options(builder, layout_align, layout_align_offset))
Expand Down
4 changes: 4 additions & 0 deletions src/pycaps/pipeline/caps_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self):
self._sound_effects: List[SoundEffect] = []
self._should_save_subtitle_data: bool = True
self._subtitle_data_path_for_loading: Optional[str] = None
self._transcription_for_loading: Optional[Document] = None
self._should_preview_transcription: bool = False
self._layout_options = SubtitleLayoutOptions()
self._preview_time: Optional[Tuple[float, float]] = None
Expand Down Expand Up @@ -254,6 +255,9 @@ def run(self) -> None:
logger().info(f"Loading subtitle data from: {self._subtitle_data_path_for_loading}")
document = SubtitleDataService(self._subtitle_data_path_for_loading).load()
self._cut_document_for_preview_time(document)
elif self._transcription_for_loading:
logger().info("Using external transcription input.")
document = self.process_document(self._transcription_for_loading)
else:
initial_document = self.transcribe()
document = self.process_document(initial_document)
Expand Down
15 changes: 14 additions & 1 deletion src/pycaps/pipeline/caps_pipeline_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from .caps_pipeline import CapsPipeline
from pycaps.layout import SubtitleLayoutOptions, LineSplitter, LayoutUpdater, PositionsCalculator
from pycaps.transcriber import AudioTranscriber, BaseSegmentSplitter, WhisperAudioTranscriber, PreviewTranscriber
from pycaps.transcriber import TranscriptFormat, load_transcription
from pycaps.common import Document
from typing import Optional
from pycaps.animation import Animation, ElementAnimator
from pycaps.common import ElementType, EventType, VideoQuality, CacheStrategy
Expand Down Expand Up @@ -75,6 +77,17 @@ def with_subtitle_data_path(self, subtitle_data_path: str) -> "CapsPipelineBuild
raise ValueError(f"Subtitle data file not found: {subtitle_data_path}")
self._caps_pipeline._subtitle_data_path_for_loading = subtitle_data_path
return self

def with_transcription(self, transcription: Document | dict | str, format: TranscriptFormat | str = TranscriptFormat.AUTO) -> "CapsPipelineBuilder":
self._caps_pipeline._transcription_for_loading = load_transcription(transcription, format)
return self

def with_transcription_file(self, path: str, format: TranscriptFormat | str = TranscriptFormat.AUTO) -> "CapsPipelineBuilder":
if not os.path.exists(path):
raise ValueError(f"Transcription file not found: {path}")
if not os.path.isfile(path):
raise ValueError(f"Transcription path is not a file: {path}")
return self.with_transcription(path, format)

def should_save_subtitle_data(self, should_save: bool) -> "CapsPipelineBuilder":
self._caps_pipeline._should_save_subtitle_data = should_save
Expand Down Expand Up @@ -121,4 +134,4 @@ def build(self, preview_time: Optional[tuple[float, float]] = None) -> CapsPipel

pipeline = self._caps_pipeline
self._caps_pipeline = CapsPipeline()
return pipeline
return pipeline
6 changes: 5 additions & 1 deletion src/pycaps/transcriber/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .editor import TranscriptionEditor
from .preview_transcriber import PreviewTranscriber
from .google_audio_transcriber import GoogleAudioTranscriber
from .transcript_format import TranscriptFormat
from .transcript_loader import load_transcription

__all__ = [
"AudioTranscriber",
Expand All @@ -15,5 +17,7 @@
"SplitIntoSentencesSplitter",
"TranscriptionEditor",
"PreviewTranscriber",
"GoogleAudioTranscriber"
"GoogleAudioTranscriber",
"TranscriptFormat",
"load_transcription",
]
9 changes: 9 additions & 0 deletions src/pycaps/transcriber/transcript_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from enum import Enum


class TranscriptFormat(str, Enum):
AUTO = "auto"
WHISPER_JSON = "whisper_json"
PYCAPS_JSON = "pycaps_json"
SRT = "srt"
VTT = "vtt"
Loading