spacr.qt.tutorial.engine
========================

.. py:module:: spacr.qt.tutorial.engine

.. autoapi-nested-parse::

   Tutorial rendering engine — narration, cursor overlay, capture, mux.

   The pipeline is intentionally linear and easy to reason about:

     1. Synthesize each Step's narration through Piper → WAV, know duration
     2. Spin up the MainWindow (on real DISPLAY or under Xvfb) at 1920x1080
     3. Walk through the Steps, capturing frames at 30 fps into a scratch dir.
        Each step gets ceil(narration_s * 30) + hold frames budget.
        A synthesized cursor (little arrow drawn onto each frame) animates
        to each step's target widget before the step's action fires.
     4. Concatenate all step WAVs → one audio track
     5. `ffmpeg -framerate 30 -i frames/%06d.png -i audio.wav ... out.mp4`
     6. Emit matching .srt sidecar

   The engine has no Qt-specific business logic — that lives in per-app
   `scripts.py` functions that return a list of Steps.









Module Contents
---------------

.. py:data:: LOG

.. py:data:: FRAME_RATE
   :value: 30


.. py:data:: VIDEO_SIZE
   :value: (1920, 1080)


.. py:data:: CURSOR_MOVE_FRAMES
   :value: 12


.. py:data:: DEFAULT_HOLD_MS
   :value: 500


.. py:data:: DEFAULT_VOICE

.. py:class:: Step

   A single narrated beat of a tutorial.

   Fields:
       narration:    what the narrator says (also becomes the subtitle)
       action:       optional callable that mutates the UI. Runs AFTER
                     the cursor animation completes but BEFORE the
                     narration finishes playing.
       target:       optional (widget, point-in-widget) the cursor
                     animates to before the action fires. Point is
                     relative to `widget`. Pass a QWidget with point
                     omitted to target its center.
       hold_ms:      extra silence at the end of the step, in ms.
                     Useful to let a UI change settle before the next
                     step begins.
       highlight:    optional widget to draw a soft highlight ring
                     around while this step runs.


   .. py:attribute:: narration
      :type:  str


   .. py:attribute:: action
      :type:  Optional[Callable[[], None]]
      :value: None



   .. py:attribute:: target
      :type:  Optional[Tuple[Any, Optional[Tuple[int, int]]]]
      :value: None



   .. py:attribute:: hold_ms
      :type:  int
      :value: 500



   .. py:attribute:: highlight
      :type:  Optional[Any]
      :value: None



.. py:class:: Narrator(voice_model: Optional[pathlib.Path] = None, length_scale: float = 1.0)

   Synthesize step narration WAVs using Piper.

   Uses the Piper CLI (already installed via pip install piper-tts).
   Voice model defaults to ~/.spacr/piper/en_US-lessac-medium.onnx
   but any Piper .onnx can be passed via `voice_model=`.


   .. py:attribute:: voice_model


   .. py:attribute:: length_scale
      :value: 1.0



   .. py:method:: synth(text: str, out_wav: pathlib.Path) -> float

      Synthesize `text` into `out_wav`. Returns duration in seconds.



.. py:class:: Recorder(window, frames_dir: pathlib.Path, fps: int = FRAME_RATE, size: Tuple[int, int] = VIDEO_SIZE)

   Grab the MainWindow's rendered pixmap N times a second,
   compositing a synthetic cursor onto each frame.

   :param window: source Qt window to grab.
   :param frames_dir: destination folder for numbered PNG frames.
   :param fps: capture frame rate.
   :param size: fixed output frame size ``(width, height)`` in px.


   .. py:attribute:: window


   .. py:attribute:: frames_dir


   .. py:attribute:: fps
      :value: 30



   .. py:attribute:: size
      :value: (1920, 1080)



   .. py:attribute:: frame_idx
      :value: 0



   .. py:attribute:: cursor_pos
      :type:  Tuple[float, float]


   .. py:method:: snap(cursor_pos: Optional[Tuple[float, float]] = None, highlight_rect: Optional[Tuple[int, int, int, int]] = None) -> pathlib.Path

      Grab one frame, save as PNG, return its path.



.. py:class:: RenderResult

   Output paths and metadata for a completed tutorial render.

   :ivar mp4: absolute path to the produced MP4.
   :ivar srt: absolute path to the produced SRT sidecar.
   :ivar frames: total number of frames captured.
   :ivar duration_s: total narration duration in seconds.


   .. py:attribute:: mp4
      :type:  pathlib.Path


   .. py:attribute:: srt
      :type:  pathlib.Path


   .. py:attribute:: frames
      :type:  int


   .. py:attribute:: duration_s
      :type:  float


.. py:class:: Director(window, steps: List[Step], out_dir: pathlib.Path, narrator: Optional[Narrator] = None, fps: int = FRAME_RATE)

   Orchestrates narration, capture, and mux into a final MP4 + SRT.

   :param window: live MainWindow the tutorial drives.
   :param steps: ordered list of :class:`Step` beats.
   :param out_dir: destination folder for the rendered mp4/srt.
   :param narrator: optional :class:`Narrator`; a default is built if omitted.
   :param fps: capture frame rate.


   .. py:attribute:: window


   .. py:attribute:: steps


   .. py:attribute:: out_dir


   .. py:attribute:: narrator


   .. py:attribute:: fps
      :value: 30



   .. py:method:: render(name: str) -> RenderResult

      Run the full narrate → capture → mux pipeline for this director.

      :param name: base filename for the produced ``<name>.mp4`` and ``.srt``.
      :returns: :class:`RenderResult` with paths and duration metadata.



.. py:function:: render_tutorial(app_key: str, out_dir: Optional[pathlib.Path] = None, voice_model: Optional[pathlib.Path] = None, length_scale: float = 1.0) -> RenderResult

   Boot MainWindow, run the per-app tutorial script, render MP4.

   Returns a RenderResult describing where the MP4 and SRT ended up.


