Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom subtitles #1467

Merged
merged 8 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions plugin/subtitles/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Subtitles

Custom subtitles with settings to tweak color, position and timeout.

Talons default subtitles needs to be disabled from the Talon menu to avoid duplicates.

![Subtitle](./images/subtitle.png)

## Show / hide subtitles

### Show subtitles

Setting: `user.subtitles_show = true`

### Hide subtitles

Setting: `user.subtitles_show = false`
Binary file added plugin/subtitles/images/subtitle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions plugin/subtitles/on_phrase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from talon import actions, speech_system
from talon.grammar import Phrase

from .subtitles import show_subtitle


def on_pre_phrase(phrase: Phrase):
if skip_phrase(phrase):
return

words = phrase["phrase"]
current_phrase = " ".join(words)
show_subtitle(current_phrase)


def skip_phrase(phrase: Phrase) -> bool:
return not phrase.get("phrase") or skip_phrase_in_sleep(phrase)


def skip_phrase_in_sleep(phrase: Phrase) -> bool:
"""Returns true if the rule is <phrase> in sleep mode"""
return (
not actions.speech.enabled()
and len(phrase["parsed"]) == 1
and phrase["parsed"][0]._name == "___ltphrase_gt__"
)


speech_system.register("phrase", on_pre_phrase)
138 changes: 138 additions & 0 deletions plugin/subtitles/subtitles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from typing import Any, Callable, Optional, Type

from talon import Module, app, cron, settings, ui
from talon.canvas import Canvas
from talon.skia.canvas import Canvas as SkiaCanvas
from talon.skia.imagefilter import ImageFilter
from talon.types import Rect

mod = Module()


def setting(
name: str, type: Type, desc: str, *, default: Optional[Any] = None
) -> Callable[[], type]:
mod.setting(f"subtitles_{name}", type, default=default, desc=f"Subtitles: {desc}")
return lambda: settings.get(f"user.subtitles_{name}")


setting_show = setting(
"show",
bool,
"If true show (custom) subtitles",
default=False,
)
setting_all_screens = setting(
"all_screens",
bool,
"If true show subtitles on all screens. If false show only on main screen.",
)
setting_size = setting(
"size",
int,
"Subtitle size in pixels",
)
setting_color = setting(
"color",
str,
"Subtitle color",
)
setting_color_outline = setting(
"color_outline",
str,
"Subtitle outline color",
)
setting_timeout_per_char = setting(
"timeout_per_char",
int,
"For each character in the subtitle extend the timeout by this amount in ms",
)
setting_timeout_min = setting(
"timeout_min",
int,
"Minimum time for a subtitle to show in ms",
)
setting_timeout_max = setting(
"timeout_max",
int,
"Maximum time for a subtitle to show in ms",
)
setting_y = setting(
"y",
float,
"Percentage of screen hight to show subtitle at. 0=top, 1=bottom",
)

mod = Module()
canvases: list[Canvas] = []


def show_subtitle(text: str):
"""Show subtitle"""
if not setting_show():
return
clear_canvases()
if setting_all_screens():
screens = ui.screens()
else:
screens = [ui.main_screen()]
for screen in screens:
canvas = show_text_on_screen(screen, text)
canvases.append(canvas)


def show_text_on_screen(screen: ui.Screen, text: str):
timeout = calculate_timeout(text)
canvas = Canvas.from_screen(screen)
canvas.register("draw", lambda c: on_draw(c, screen, text))
canvas.freeze()
cron.after(f"{timeout}ms", canvas.close)
return canvas


def on_draw(c: SkiaCanvas, screen: ui.Screen, text: str):
scale = screen.scale if app.platform != "mac" else 1
size = setting_size() * scale
rect = set_text_size_and_get_rect(c, size, text)
x = c.rect.center.x - rect.center.x
# Clamp coordinate to make sure entire text is visible
y = max(
min(
c.rect.y + setting_y() * c.rect.height + c.paint.textsize / 2,
c.rect.bot - rect.bot,
),
c.rect.top - rect.top,
)

c.paint.imagefilter = ImageFilter.drop_shadow(2, 2, 1, 1, "000000")
c.paint.style = c.paint.Style.FILL
c.paint.color = setting_color()
c.draw_text(text, x, y)

# Outline
c.paint.imagefilter = None
c.paint.style = c.paint.Style.STROKE
c.paint.color = setting_color_outline()
c.draw_text(text, x, y)


def calculate_timeout(text: str) -> int:
ms_per_char = setting_timeout_per_char()
ms_min = setting_timeout_min()
ms_max = setting_timeout_max()
return min(ms_max, max(ms_min, len(text) * ms_per_char))


def set_text_size_and_get_rect(c: SkiaCanvas, size: int, text: str) -> Rect:
while True:
c.paint.textsize = size
rect = c.paint.measure_text(text)[1]
if rect.width < c.width * 0.8:
return rect
size *= 0.9


def clear_canvases():
for canvas in canvases:
canvas.close()
canvases.clear()
19 changes: 19 additions & 0 deletions plugin/subtitles/subtitles.talon
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
settings():
# Show subtitles
user.subtitles_show = false
# Show subtitles on all screens
user.subtitles_all_screens = true
# 100px subtitles font size
user.subtitles_size = 100
# White subtitles color
user.subtitles_color = "ffffff"
# Slightly dark subtitle outline
user.subtitles_color_outline = "aaaaaa"
# For each character in the subtitle extend the timeout 50ms
user.subtitles_timeout_per_char = 50
# 750ms is the minimum timeout for a subtitle
user.subtitles_timeout_min = 750
# 3 seconds is the maximum timeout for a subtitle
user.subtitles_timeout_max = 3000
# Subtitles are positioned at the bottom of the screen
user.subtitles_y = 0.93