Ir para o conteúdo

Project

VideoProjectConfig

Bases: BaseModel

A dictionary representing the configuration of a project.

title class-attribute instance-attribute

title: str = 'Untitled Project'

The title of the project. Defaults to "Untitled Project".

version class-attribute instance-attribute

version: int = 1

The version of the project. Defaults to 1.

resolution class-attribute instance-attribute

resolution: FrameSize = (1920, 1080)

The resolution of the project in pixels. Defaults to 1920x1080.

fps class-attribute instance-attribute

fps: PositiveInt = 30

The frames per second of the project. Defaults to 30.

VideoProject

Bases: BaseModel

Represents a project with various properties and methods to manipulate its data.

config class-attribute instance-attribute

config: VideoProjectConfig = Field(
    default_factory=VideoProjectConfig
)

The configuration of the project.

assets class-attribute instance-attribute

assets: dict[str, Asset] = Field(default_factory=dict)

A dictionary mapping assets keys to Asset objects.

timeline class-attribute instance-attribute

timeline: Timeline = Field(default_factory=Timeline)

The timeline of assets and scenes of the video.

duration property

duration: float

The total duration of the project in seconds.

from_dict classmethod

from_dict(data: dict[str, Any]) -> VideoProject

Create a Project object from a dictionary.

Parameters:

Name Type Description Default
data dict[str, Any]

The dictionary containing the project data.

required

Returns:

Type Description
VideoProject

A Project object instance.

Source code in src/mosaico/video/project.py
@classmethod
def from_dict(cls, data: dict[str, Any]) -> VideoProject:
    """
    Create a Project object from a dictionary.

    :param data: The dictionary containing the project data.
    :return: A Project object instance.
    """
    config = data.get("config", VideoProjectConfig())
    project = cls(config=config)

    if "assets" in data:
        project.add_assets(data["assets"])

    if "timeline" in data:
        project.add_timeline_events(data["timeline"])

    return project

from_file classmethod

from_file(path: PathLike) -> VideoProject

Create a Project object from a YAML file.

Parameters:

Name Type Description Default
path PathLike

The path to the YAML file.

required

Returns:

Type Description
VideoProject

A Project object instance.

Source code in src/mosaico/video/project.py
@classmethod
def from_file(cls, path: PathLike) -> VideoProject:
    """
    Create a Project object from a YAML file.

    :param path: The path to the YAML file.
    :return: A Project object instance.
    """
    project_str = Path(path).read_text(encoding="utf-8")
    project_dict = yaml.safe_load(project_str)
    return cls.from_dict(project_dict)

from_script_generator classmethod

from_script_generator(
    script_generator: ScriptGenerator,
    media: Sequence[Media],
    *,
    config: VideoProjectConfig | None = None,
    **kwargs: Any
) -> VideoProject

Create a Project object from a script generator.

Parameters:

Name Type Description Default
generator

The script generator to use.

required
media Sequence[Media]

The media files to use.

required
config VideoProjectConfig | None

The configuration of the project.

None
kwargs Any

Additional keyword arguments to pass to the script generator.

{}

Returns:

Type Description
VideoProject

A Project object instance.

Source code in src/mosaico/video/project.py
@classmethod
def from_script_generator(
    cls,
    script_generator: ScriptGenerator,
    media: Sequence[Media],
    *,
    config: VideoProjectConfig | None = None,
    **kwargs: Any,
) -> VideoProject:
    """
    Create a Project object from a script generator.

    :param generator: The script generator to use.
    :param media: The media files to use.
    :param config: The configuration of the project.
    :param kwargs: Additional keyword arguments to pass to the script generator.
    :return: A Project object instance.
    """
    config = config if config is not None else VideoProjectConfig()
    project = cls(config=config)

    # Generate assets and scenes from a scene generator.
    script = script_generator.generate(media, **kwargs)

    # Create assets and scenes from the script.
    for shot in script.shots:
        # Create subtitle asset
        shot_subtitle = SubtitleAsset.from_data(shot.subtitle)

        # Create scene with initial subtitle reference
        scene = Scene(description=shot.description).add_asset_references(
            AssetReference.from_asset(shot_subtitle).with_start_time(shot.start_time).with_end_time(shot.end_time)
        )

        # Add subtitle asset to project
        project = project.add_assets(shot_subtitle)

        # Process each media reference in the shot
        for media_ref in shot.media_references:
            # Find the referenced media
            referenced_media = next(m for m in media if m.id == media_ref.media_id)

            # Convert media to asset
            media_asset = convert_media_to_asset(referenced_media)

            # Create asset reference with timing and effects
            asset_ref = (
                AssetReference.from_asset(media_asset)
                .with_start_time(media_ref.start_time)
                .with_end_time(media_ref.end_time)
            )

            # Add effects if it's an image asset
            if media_asset.type == "image" and media_ref.effects:
                asset_ref = asset_ref.with_effects([create_effect(effect) for effect in media_ref.effects])

            # Add media asset and its reference to the scene
            project = project.add_assets(media_asset)
            scene = scene.add_asset_references(asset_ref)

        # Add completed scene to project timeline
        project = project.add_timeline_events(scene)

    return project

to_file

to_file(path: PathLike) -> None

Write the Project object to a YAML file.

Parameters:

Name Type Description Default
path PathLike

The path to the YAML file.

required
Source code in src/mosaico/video/project.py
def to_file(self, path: PathLike) -> None:
    """
    Write the Project object to a YAML file.

    :param path: The path to the YAML file.
    """
    project = self.model_dump(exclude_none=True)
    project["assets"] = {asset_id: asset.model_dump() for asset_id, asset in self.assets.items()}
    project["timeline"] = [event.model_dump() for event in self.timeline]
    project_yaml = yaml.safe_dump(project, allow_unicode=True, sort_keys=False)
    Path(path).write_text(project_yaml)

add_assets

add_assets(assets: AssetInputType) -> VideoProject

Add one or more assets to the project.

Parameters:

Name Type Description Default
assets AssetInputType

The asset or list of assets to add.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def add_assets(self, assets: AssetInputType) -> VideoProject:
    """
    Add one or more assets to the project.

    :param assets: The asset or list of assets to add.
    :return: The updated project.
    """
    _assets = assets if isinstance(assets, Sequence) else [assets]

    for asset in _assets:
        if not isinstance(asset, Mapping):
            self.assets[asset.id] = asset
            continue

        if not isinstance(asset, Mapping):
            msg = f"Invalid asset type: {type(asset)}"
            raise ValueError(msg)

        if "type" not in asset:
            self.assets.update({a.id: a for a in _process_asset_dicts(asset)})
            continue

        asset = _process_single_asset_dict(asset)
        self.assets[asset.id] = asset

    return self

add_timeline_events

add_timeline_events(
    events: EventOrEventSequence,
) -> VideoProject

Add one or more events to the timeline.

Parameters:

Name Type Description Default
events EventOrEventSequence

The event or list of events to add.

required

Returns:

Type Description
VideoProject

The updated project.

Raises:

Type Description
ValueError

If an asset referenced in the events does not exist in the project.

Source code in src/mosaico/video/project.py
def add_timeline_events(self, events: EventOrEventSequence) -> VideoProject:
    """
    Add one or more events to the timeline.

    :param events: The event or list of events to add.
    :return: The updated project.
    :raises ValueError: If an asset referenced in the events does not exist in the project.
    """

    def validate_asset_id(asset_id: str, context: str = "") -> None:
        """Helper to validate asset ID exists"""
        if asset_id not in self.assets:
            msg = f"Asset with ID '{asset_id}' {context}not found in project assets."
            raise IndexError(msg)

    def validate_scene_assets(scene_event: Scene | Mapping[str, Any]) -> None:
        """Helper to validate assets referenced in a scene"""
        if isinstance(scene_event, Scene):
            for ref in scene_event.asset_references:
                validate_asset_id(ref.asset_id, "referenced in scene ")
        else:
            for ref in scene_event["asset_references"]:
                if isinstance(ref, AssetReference):
                    validate_asset_id(ref.asset_id, "referenced in scene ")
                else:
                    validate_asset_id(ref["asset_id"], "referenced in scene ")

    # Convert single event to list
    _events = events if isinstance(events, Sequence) else [events]

    # Validate all asset references exist
    for event in _events:
        if isinstance(event, Scene):
            validate_scene_assets(event)
        elif isinstance(event, AssetReference):
            validate_asset_id(event.asset_id)
        elif isinstance(event, Mapping):
            if "asset_references" in event:
                validate_scene_assets(event)
            else:
                validate_asset_id(event["asset_id"])

    self.timeline = self.timeline.add_events(events).sort()

    return self

add_narration

add_narration(
    speech_synthesizer: SpeechSynthesizer,
) -> VideoProject

Add narration to subtitles inside Scene objects by generating speech audio from subtitle text.

Updates asset timings within each Scene to match narration duration, dividing time equally between multiple images.

Parameters:

Name Type Description Default
speech_synthesizer SpeechSynthesizer

The speech synthesizer to use for generating narration audio

required

Returns:

Type Description
VideoProject

The updated project with narration added

Source code in src/mosaico/video/project.py
def add_narration(self, speech_synthesizer: SpeechSynthesizer) -> VideoProject:
    """
    Add narration to subtitles inside Scene objects by generating speech audio from subtitle text.

    Updates asset timings within each Scene to match narration duration, dividing time equally
    between multiple images.

    :param speech_synthesizer: The speech synthesizer to use for generating narration audio
    :return: The updated project with narration added
    """
    current_time = None

    for i, scene in enumerate(self.timeline.sort()):
        if not isinstance(scene, Scene):
            continue

        # Get subtitle content from scene
        subtitle_refs = [ref for ref in scene.asset_references if ref.asset_type == "subtitle"]

        if not subtitle_refs:
            continue

        # Get subtitle assets and their text content
        subtitle_assets = [cast(SubtitleAsset, self.get_asset(ref.asset_id)) for ref in subtitle_refs]

        # Generate narration for subtitle content
        texts = [subtitle.to_string() for subtitle in subtitle_assets]
        narration_assets = speech_synthesizer.synthesize(texts)

        # Add narration assets to project
        self.add_assets(narration_assets)

        # Calculate total narration duration for this scene
        total_narration_duration = sum(narration.duration for narration in narration_assets)

        # Get non-subtitle assets to adjust timing
        non_subtitle_refs = [ref for ref in scene.asset_references if ref.asset_type != "subtitle"]
        image_refs = [ref for ref in non_subtitle_refs if ref.asset_type == "image"]
        other_refs = [ref for ref in non_subtitle_refs if ref.asset_type != "image"]

        if current_time is None:
            current_time = scene.asset_references[0].start_time

        new_refs = []

        # Adjust image timings - divide narration duration equally
        if image_refs:
            time_per_image = total_narration_duration / len(image_refs)
            for idx, ref in enumerate(image_refs):
                new_start = current_time + (idx * time_per_image)
                new_end = new_start + time_per_image
                new_ref = ref.model_copy().with_start_time(new_start).with_end_time(new_end)
                new_refs.append(new_ref)

        # Add other non-image assets with full narration duration
        for ref in other_refs:
            new_ref = (
                ref.model_copy()
                .with_start_time(current_time)
                .with_end_time(current_time + total_narration_duration)
            )
            new_refs.append(new_ref)

        # Add subtitle references spanning full narration duration
        for ref in subtitle_refs:
            new_ref = (
                ref.model_copy()
                .with_start_time(current_time)
                .with_end_time(current_time + total_narration_duration)
            )
            new_refs.append(new_ref)

        # Add narration references
        for narration in narration_assets:
            narration_ref = (
                AssetReference.from_asset(narration)
                .with_start_time(current_time)
                .with_end_time(current_time + narration.duration)
            )
            new_refs.append(narration_ref)

        # Update current_time for next scene
        current_time += total_narration_duration

        # Create new scene with updated references
        new_scene = scene.model_copy(update={"asset_references": new_refs})
        self.timeline[i] = new_scene

    return self

add_captions

add_captions(
    transcription: Transcription,
    *,
    max_duration: int = 5,
    params: TextAssetParams | None = None,
    scene_index: int | None = None,
    overwrite: bool = False
) -> VideoProject

Add subtitles to the project from a transcription.

Parameters:

Name Type Description Default
transcription Transcription

The transcription to add subtitles from.

required
max_duration int

The maximum duration of each subtitle.

5
params TextAssetParams | None

The parameters for the subtitle assets.

None
scene_index int | None

The index of the scene to add the subtitles to.

None
overwrite bool

Whether to overwrite existing subtitles in the scene.

False

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def add_captions(
    self,
    transcription: Transcription,
    *,
    max_duration: int = 5,
    params: TextAssetParams | None = None,
    scene_index: int | None = None,
    overwrite: bool = False,
) -> VideoProject:
    """
    Add subtitles to the project from a transcription.

    :param transcription: The transcription to add subtitles from.
    :param max_duration: The maximum duration of each subtitle.
    :param params: The parameters for the subtitle assets.
    :param scene_index: The index of the scene to add the subtitles to.
    :param overwrite: Whether to overwrite existing subtitles in the scene.
    :return: The updated project.
    """
    subtitles = []
    references = []

    phrases = _group_transcript_into_sentences(transcription, max_duration=max_duration)

    if scene_index is not None:
        scene = self.timeline[scene_index]

        if scene.has_subtitles and not overwrite:
            msg = f"Scene at index {scene_index} already has subtitles. Use `overwrite=True` to replace."
            raise ValueError(msg)

        # Remove existing subtitles
        for ref in scene.asset_references:
            if ref.asset_type == "subtitle":
                self.remove_asset(ref.asset_id)

        # Calculate time scale factor if needed
        current_time = scene.start_time

        for phrase in phrases:
            subtitle_text = " ".join(word.text for word in phrase)
            subtitle = SubtitleAsset.from_data(subtitle_text)

            # Calculate scaled duration
            phrase_duration = phrase[-1].end_time - phrase[0].start_time

            start_time = current_time
            end_time = start_time + phrase_duration

            # Ensure we don't exceed scene bounds
            end_time = min(end_time, scene.end_time)

            subtitle_ref = AssetReference.from_asset(
                asset=subtitle,
                asset_params=params,
                start_time=start_time,
                end_time=end_time,
            )
            subtitles.append(subtitle)
            references.append(subtitle_ref)

            current_time = end_time

        self.add_assets(subtitles)
        scene = scene.add_asset_references(references)
        self.timeline[scene_index] = scene
    else:
        # Handle non-scene case
        for phrase in phrases:
            subtitle_text = " ".join(word.text for word in phrase)
            subtitle = SubtitleAsset.from_data(subtitle_text)

            subtitle_ref = AssetReference.from_asset(
                asset=subtitle,
                asset_params=params,
                start_time=phrase[0].start_time,
                end_time=phrase[-1].end_time,
            )
            subtitles.append(subtitle)
            references.append(subtitle_ref)

        self.add_assets(subtitles)
        self.add_timeline_events(references)

    return self

add_captions_from_transcriber

add_captions_from_transcriber(
    audio_transcriber: AudioTranscriber,
    *,
    max_duration: int = 5,
    params: TextAssetParams | None = None,
    overwrite: bool = False
) -> VideoProject

Add subtitles to the project from audio assets using an audio transcriber.

Parameters:

Name Type Description Default
audio_transcriber AudioTranscriber

The audio transcriber to use for transcribing audio assets.

required
max_duration int

The maximum duration of each subtitle.

5
params TextAssetParams | None

The parameters for the subtitle assets.

None
overwrite bool

Whether to overwrite existing subtitles in the scene.

False

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def add_captions_from_transcriber(
    self,
    audio_transcriber: AudioTranscriber,
    *,
    max_duration: int = 5,
    params: TextAssetParams | None = None,
    overwrite: bool = False,
) -> VideoProject:
    """
    Add subtitles to the project from audio assets using an audio transcriber.

    :param audio_transcriber: The audio transcriber to use for transcribing audio assets.
    :param max_duration: The maximum duration of each subtitle.
    :param params: The parameters for the subtitle assets.
    :param overwrite: Whether to overwrite existing subtitles in the scene.
    :return: The updated project.
    """
    for i, event in enumerate(self.timeline):
        if not isinstance(event, Scene) or not event.has_audio:
            continue

        for asset_ref in event.asset_references:
            if asset_ref.asset_type != "audio":
                continue

            audio_asset = self.get_asset(asset_ref.asset_id)
            audio_asset = cast(AudioAsset, audio_asset)
            audio_transcription = audio_transcriber.transcribe(audio_asset)

            self.add_captions(
                audio_transcription,
                max_duration=max_duration,
                params=params,
                scene_index=i,
                overwrite=overwrite,
            )

    return self

with_subtitle_params

with_subtitle_params(
    params: TextAssetParams | Mapping[str, Any]
) -> VideoProject

Override the subtitle parameters for the assets in the project.

Parameters:

Name Type Description Default
params TextAssetParams | Mapping[str, Any]

The subtitle parameters to set.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def with_subtitle_params(self, params: TextAssetParams | Mapping[str, Any]) -> VideoProject:
    """
    Override the subtitle parameters for the assets in the project.

    :param params: The subtitle parameters to set.
    :return: The updated project.
    """
    if not self.timeline:
        msg = "The project timeline is empty."
        raise ValueError(msg)

    params = TextAssetParams.model_validate(params)

    for i, event in enumerate(self.timeline):
        if isinstance(event, Scene):
            self.timeline[i].with_subtitle_params(params)
        elif isinstance(event, AssetReference):
            self.timeline[i].asset_params = params

    return self

with_title

with_title(title: str) -> VideoProject

Override the title of the project.

Parameters:

Name Type Description Default
title str

The title to set.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def with_title(self, title: str) -> VideoProject:
    """
    Override the title of the project.

    :param title: The title to set.
    :return: The updated project.
    """
    self.config.title = title
    return self

with_version

with_version(version: int) -> VideoProject

Override the project version.

Parameters:

Name Type Description Default
version int

The version to set.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def with_version(self, version: int) -> VideoProject:
    """
    Override the project version.

    :param version: The version to set.
    :return: The updated project.
    """
    self.config.version = version
    return self

with_fps

with_fps(fps: int) -> VideoProject

Override the FPS of the project.

Parameters:

Name Type Description Default
fps int

The FPS to set.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def with_fps(self, fps: int) -> VideoProject:
    """
    Override the FPS of the project.

    :param fps: The FPS to set.
    :return: The updated project.
    """
    self.config.fps = fps
    return self

with_resolution

with_resolution(resolution: FrameSize) -> VideoProject

Override the resolution of the project.

Parameters:

Name Type Description Default
resolution FrameSize

The resolution to set.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def with_resolution(self, resolution: FrameSize) -> VideoProject:
    """
    Override the resolution of the project.

    :param resolution: The resolution to set.
    :return: The updated project.
    """
    self.config.resolution = resolution
    return self

get_asset

get_asset(asset_id: str) -> Asset

Get an asset by its ID.

Parameters:

Name Type Description Default
asset_id str

The ID of the asset.

required

Returns:

Type Description
Asset

The Asset object.

Raises:

Type Description
ValueError

If the asset is not found in the project assets.

Source code in src/mosaico/video/project.py
def get_asset(self, asset_id: str) -> Asset:
    """
    Get an asset by its ID.

    :param asset_id: The ID of the asset.
    :return: The Asset object.
    :raises ValueError: If the asset is not found in the project assets.
    """
    try:
        return self.assets[asset_id]
    except KeyError:
        msg = f"Asset with ID '{asset_id}' not found in the project assets."
        raise KeyError(msg) from None

get_timeline_event

get_timeline_event(index: int) -> TimelineEvent

Get a timeline event by its index.

Parameters:

Name Type Description Default
index int

The index of the timeline event.

required

Returns:

Type Description
TimelineEvent

The TimelineEvent object.

Raises:

Type Description
ValueError

If the index is out of range.

Source code in src/mosaico/video/project.py
def get_timeline_event(self, index: int) -> TimelineEvent:
    """
    Get a timeline event by its index.

    :param index: The index of the timeline event.
    :return: The TimelineEvent object.
    :raises ValueError: If the index is out of range.
    """
    try:
        return self.timeline[index]
    except IndexError:
        msg = "Index out of range."
        raise IndexError(msg) from None

remove_asset

remove_asset(asset_id: str) -> VideoProject

Remove an asset from the project.

Parameters:

Name Type Description Default
asset_id str

The ID of the asset to remove.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def remove_asset(self, asset_id: str) -> VideoProject:
    """
    Remove an asset from the project.

    :param asset_id: The ID of the asset to remove.
    :return: The updated project.
    """
    try:
        for i, event in enumerate(self.timeline):
            if isinstance(event, Scene):
                self.timeline[i] = event.remove_references_by_asset_id(asset_id)
            elif isinstance(event, AssetReference) and event.asset_id == asset_id:
                del self.timeline[i]
        del self.assets[asset_id]
        return self
    except KeyError:
        msg = f"Asset with ID '{asset_id}' not found in the project assets."
        raise KeyError(msg) from None

remove_timeline_event

remove_timeline_event(index: int) -> VideoProject

Remove a timeline event from the project.

Parameters:

Name Type Description Default
index int

The index of the timeline event to remove.

required

Returns:

Type Description
VideoProject

The updated project.

Source code in src/mosaico/video/project.py
def remove_timeline_event(self, index: int) -> VideoProject:
    """
    Remove a timeline event from the project.

    :param index: The index of the timeline event to remove.
    :return: The updated project.
    """
    try:
        del self.timeline[index]
        return self
    except IndexError:
        msg = "Timeline event index out of range."
        raise IndexError(msg) from None