螢幕錄製與字幕合成

螢幕錄製的軟體很多,像是 FonePaw、Soundflower、ApowerREC、Nonosnap、Kap、Camtasia Studio、iShowU HD 等等的,我是使用 Quicktime player 。

執行 Quicktime palyer 以後,會要你選存放位置,我是不管他,按「取消」。用滑鼠點按「檔案」 > 「新增螢幕錄製」,接著畫面會出現一個工具列視窗,請先選定錄製全螢幕或是錄製指定區域,然後再按下選項:

  • 勾選使用 MacBookPro 的麥克風
  • 勾選滑鼠點按

這邊可以加選計時器,這可以讓錄製的動作在指定秒數後開始,按下錄製就可以開始錄製影片了。錄製的時候,要注意找個比較安靜的地方,說話的聲音也要提高音量,這樣錄製的效果會比較好。

錄製完成以後,可以使用 iMovie 編輯,在 iMovie 裡可以加入過場動畫,調整音量,也可以逐句上字幕。調整音量是點按上方選單的「修改」> 「增強」

因為我懶得逐句上字幕,所以使用別的方式。首先先把製作好的影片匯出,先回到主畫面的計劃案,點選你計劃案右下角的 「…」,選「分享計劃案」>「檔案」,這樣就可以把檔案匯出到指定資料夾去了。

再來是抽取出音訊,先安裝 ffmpeg :brew install ffmpeg

在終端機輸入 ffmpeg -i xxx.mp4 xxx.mp3

要調整聲音的話,也可以在這邊作(參考自ffmpeg的wiki):ffmpeg -i input.wav -filter:a “volume=10dB” output.wav

取出以後,再來是試著從音訊轉出字幕,我試了兩個方法,第一個方法是用布丁大大web speech to text ,這有蠻多教學文章的:

在這邊就可以上傳音訊檔案,然後播放時,就會自動轉出字幕,這邊由於是利用到播放跟麥克風,所以在轉換時,一樣需要一個比較安靜的地方,同時音訊檔的音量也需要大一點,轉出來效果會比較好。

第二個方法是自己手打,因為環境有點吵雜的關係,我後來是用這方法。這邊是用了 oTranscribe 這個網站,一樣是要上傳音訊,oTranscribe 提供了便利的介面,讓你可以邊聽邊打。完成以後,可以匯出文字檔。這邊大致講一下怎麼使用:

  • ESC: Play/Pauser
  • F1: 倒轉
  • F2: 快轉
  • Ctrl +j (在Mac是用Command+j) : 插入時間

如果你需要教學的話,這裡有:oTranscribe 線上逐字稿工具,支援讀取電腦影片製作字幕

取得文字檔以後,還需要轉換為通用的 srt 檔案,這一樣有網站提供這功能: Subtitle tools – Convert Subtitles to Srt

將剛剛取得的文字檔上傳上去,按下「Convert to Srt」,然後就可以下載了。

最後是將字幕合併到影片裡。

  1. 安裝 handbrake: brew cask install handbrake
  2. 從啟動台找到 HandBrake,執行
  3. 選擇前面匯出的 mp4,再選擇 “Subtitles” 頁籤,點按「Tracks」> 「Add External Subtitles track」加入前面取得的 .srt 檔案
  4. 調整字幕的語言,再勾選 Burned In 
  5. 在下方填入輸出的檔名,並選擇要輸出的資料夾。
  6. 按下上方的「Start」

需要教學的話,一樣有:SRT 字幕筆記

這樣就完成啦。

其他的參考資料:

Angular 嵌入 redoc API 文件

原本是打算把 redoc-cli 產生出來的文件直接嵌進去的,可是這樣子在文件有變動時,就會又要再做一次產生、嵌入,這樣不太好。最好還是可以自動依據寫好的 OpenAPI specification 來自動產出,這才是比較好的作法。

第一個待解決的問題是怎麼把 OpenAPI specification JSON 放到 Angular 專案裡,並且可以讀出來使用。關於這個,我是找到 How To Read Local JSON Files In Angular 這篇文章,方法挺簡單的,就把 json 檔案丟到 src/assets 下,然後直接在程式裡用 import 。

// src/app/xxx/xxx.component.ts
import SampleJson from '../../assets/SampleJson.json’;

// 後續程式碼就直接引用 SampleJSON 即可。

但修改完,TypeScript compiler 會有錯誤訊息,這得要改 tsconfig.json 在 compilerOptions 裡加入 resolveJsonModule 跟 esModuleInterop

{ "compilerOptions": { "resolveJsonModule": true, "esModuleInterop": true } }

第二個問題是,angular template 裡不能直接寫 script tag,這個可以實作 AfterViewInit ,然後用 DOMElement 來動態插入 (參考來源:https://stackoverflow.com/questions/38088996/adding-script-tags-in-angular-component-template/43559644)。細節的說明,我就直接寫在下面程式的註解裡。

import {
  Component,
  OnInit,
  ViewEncapsulation,
  ElementRef,
  AfterViewInit } from '@angular/core’;
// (1) 剛剛前面提到的,引用 OpenAPI specification JSON
import APIJson from '../../assets/api.json';

@Component({
  selector: 'app-documentation',
  templateUrl: './documentation.component.html',
  styleUrls: ['./documentation.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class DocumentationComponent implements OnInit, AfterViewInit {  // (2) Component 要實作 AfterViewInit 這介面

  constructor(private elementRef: ElementRef) { }  // (3) 引入 ElementRef ,Angular 會幫忙作動態注入

  ngAfterViewInit(): void {
    // (4) 這裡就是在頁面載入後要作的事情
    this.insertSpec();
    this.insertScript();
  }

  insertScript(): void {
    // (5) 插入 script element,這裡要載入 redoc 的腳本
    const redocScript = document.createElement('script');
    redocScript.type = 'text/javascript';
    redocScript.src = 'https://unpkg.com/redoc@next/bundles/redoc.standalone.js’;  // (6) 這腳本的網址是閱讀 redoc-cli 的原始碼以後取得的。
    this.elementRef.nativeElement.appendChild(redocScript);

    // (7) 插入另外一個 script element,這裡主要是執行 redoc,讓 redoc 能解析 json 並顯示出文件。
    const initScript = document.createElement('script');
    initScript.type = 'text/javascript';
    initScript.textContent = `
    const __redoc_state=${JSON.stringify(APIJson)};
    var container = document.getElementById('redoc');
    Redoc.hydrate(__redoc_state, container);
    `;  // (8) 字串要允許多行,可以使用 backquote ` https://stackoverflow.com/questions/35225399/multiline-string
    this.elementRef.nativeElement.appendChild(initScript);
  }

  insertSpec(): void {
    // (9) 插入 element ,讓腳本知道要根據哪個 element 來作處理。
    let s = document.createElement('redoc');
    s.setAttribute('spec-url', 'assets/api.yml');
    this.elementRef.nativeElement.appendChild(s);
  }

  ngOnInit(): void {
  }
}

至此就大功告成啦。

P.S. redoc 在 1.x 時,是有支援 Angular 的,但到了 2.x ,就改用 react 了。

不是只有 console.log

這篇 JavaScript console is more than console.log() 講了幾種 console 的用法,console 不是只有 log 可以用。

  1. console.warn() / console.error() 可以依據訊息等級顯示出不同的顏色。
  2. console.group() / console.groupEnd() 可以讓這中間的 log 輸出都摺疊起來,避免太多。
  3. console.table() 是印出表格,表格的形式比較容易查看資料內容。
  4. console.trace() 是印出 stacktrace 資訊
  5. console.time() / console.timeEnd() 可以輸出這中間的指令花了多少時間。
  6. console.clear() 清理終端機輸出內容
  7. console.dir() 可以印出物件的屬性。

另外,現在瀏覽器的 js 都支援 backquote – template literals ,也就是 ` ` ,所以也可以這樣用 console.log(`obj=${JSON.stringify(x)}`);

電影流水帳(2020/03/26~2020/04/10)

anushka shetty hot dancing stills (10)
Anushka Shetty ,飾演提婆犀那,濕婆度。巴霍巴利的妻子。
  • बाहुबली: द बिगनिंग  (IMDB, Wikipedia),台譯:帝國戰神-巴霍巴利王。
  • बाहुबली 2: द कॉन्क्लूज़न (IMDB, Wikipedia),台譯:巴霍巴利王-磅礴終章。

बाहुबली: द बिगनिंग (帝國戰神:巴霍巴利王)

真的蠻好看的,雖然特效、動作什麼的都很誇張,不過好萊塢電影也是這樣,不是嗎?

這一集主要是講巴霍巴利的兒子-亞美德拉,他在一個偏僻的地方長大,不知道自己的身世,總想著要爬上瀑布頂端看看,可是這瀑布非常險峻,他總是沒能成功。有一天他看到從瀑布那邊飄下一個面具,就開始盯著這個面具看,猜想著,面具的背後應該是個美女吧。於是決定,拼了命也要爬上去。這一天,他成功的爬上去了,真的看到美女,從這個美女的組織,他開始了新的旅程。這個美女的組織其實是反叛軍,他們想要救出巴霍巴利的妻子,也就是公主,於是亞美德拉在不知情的情況下,幫助了反叛軍進到帕拉提婆王城,並救出公主。這時候亞美德拉的母親也趕來,亞美德拉這時心裡感到很奇怪,明明照顧自己長大的母親就在旁邊,可是為什麼看到公主,感覺非常的親切、很熟悉,就好像是媽媽?然後為什麼大家在那邊鬼叫著巴霍巴利?

這時候,帕拉提婆派來追殺亞美德拉的卡塔帕娓娓道出這段過去。過去這個國家是由席娃伽彌暫管的,他撫養並培育自己丈夫兄弟的兒子-濕婆度跟自己的兒子-帕拉提婆長大,這兩個兒子都非常的聰明以及強壯。有一天,克拉卡雅這個部落來攻打,一場大戰下來,眼看已經趨於劣勢,這時候濕婆度重新帶起軍心,從而打敗了克拉卡雅。席娃伽彌在戰後,決定讓濕婆度當這個王國的國王。亞美德拉這時就問了,那照理來說,應該這個王國就過著快樂的生活才對,現在怎麼會變成這樣子?濕婆度怎麼會死去?卡塔帕這時很沈痛的說,那是因為我…. 沒錯,請看下集的故事。

बाहुबली 2: द कॉन्क्लूज़न (巴霍巴利王-磅礴終章)

這一集就開始講述亞美德拉的父親-巴霍巴利的故事。話說上次大勝克拉卡雅,太后席娃伽彌決定讓巴霍巴利當未來國王以後,接下來就是成親跟即位的事情了。太后告訴巴霍巴利,在即位前的這段時間,你出去走走,了解一下我們的領土。巴霍巴利就聽從太后建議,跟卡塔帕一同微服去探訪民間了。在路上,巴霍巴利遇到一個剽悍的公主,一見鍾情,就跟卡塔帕冒充為平民,混入這個小王國裡。在這個小王國廝混的時間裡,他取得了公主的好感。

巴霍巴利的弟弟帕拉提婆的嘴裏是跟太后說,自己心甘情願當個大將軍,但是心裡其實很不爽。在即位前的這段時間,怎麼可能放著巴霍巴利在那邊趴趴走呢?他知道巴霍巴利那個小王國愛上了公主提婆犀那以後,趕緊去跟太后說,我想結婚了,對象是提婆犀那。太后在不知情的狀況下,許了這個承諾,說會去提親。這個提親的過程並不圓滿,太后這方的使者過於高傲,公主提婆犀那因為這個高傲的態度,也不爽的頂撞回去,兩方有了小小的不滿,太后就派了軍隊去攻打這個小王國。

在軍隊來襲的晚上,巴霍巴利聽到熟悉的號角聲,出面警告了這個小王國,並且進而帶領這個小王國打退了太后派來的軍隊。這個時候,巴霍巴利還以為太后來提親是因為自己,就把事情都講清楚,說明自己的真心,說會好好的照顧提婆犀那。提婆犀那也相信了巴霍巴利,就跟巴霍巴利回去了。回去以後,去稟告太后,巴霍巴利這時候才知道,新郎不是我,是弟弟!太后說,你要娶提婆犀那可以,但是你就不用當國王了。巴霍巴利為了遵守對提婆犀那的承諾,就決定放下王位,走出王宮。作為平民的巴霍巴利與提婆犀那過著平靜的日子,很受人民愛戴。帕拉提婆雖然得到了王位,看到哥哥受到人民愛戴,總覺得芒刺在身。

轉眼,提婆犀那懷了身孕,太后在知道消息以後,心有點軟了,就召喚他們倆回來宮里。帕拉提婆跟他的父親想出了另外一個計謀,構陷了巴霍巴利派人來刺殺自己的事情。於是他們要求卡塔帕去遙遠的地方,並讓太后派出巴霍巴利去救卡塔帕,支開巴霍巴利。巴霍巴利趕去救卡塔帕,見到卡塔帕時,卡塔帕知道是帕拉提婆的陷阱,跟巴霍巴利說你其實不需要救我的。但是對巴霍巴利來說,卡塔帕除了是王室的僕人,也是自己的家人,他照護太后,也照護自己長大,更何況,他還幫自己找到了真愛,怎麼能不救呢?於是巴霍巴利救出了卡塔帕並跟刺客奮戰。可是啊,就在要殺出重圍時,卡塔帕捅了巴霍巴利。怎麼會這樣呢?卡塔帕怎麼忍心下這毒手,原來,帕拉提婆以太后的性命要脅卡塔帕聽命於自己,卡塔帕無奈,只能聽從。

巴霍巴利去救卡塔帕的這天晚上,「有刺客、有刺客」這樣的聲音在王宮裡迴盪著,在王宮裡引起了騷動。是時,提婆犀那也生下他的孩子,帕拉提婆趁這個機會發動了政變並準備殺害提婆犀那跟他的孩子,太后看清了自己兒子-帕拉提婆的真面目,才知道自己的兒子居心叵測。她找到自己的媳婦提婆犀那,聽從提婆犀那的話,將男嬰帶走,跳入河中。大雨之中,河水非常湍急,將這一老一少衝到了很遠的地方去。太后以自己的性命保護了這個男嬰,並把男嬰交付給一個女人。提婆犀那沒辦法逃走,也不願意屈從於帕拉提婆,就被囚禁在王宮之中。

接著故事就回到第一集,這個小男孩,也就是亞美德拉,長大了,因緣際會下回到了王宮。聽完整個故事,亞美德拉原諒了卡塔帕,帶領著眾人攻打帕拉提婆。經過一場大戰,亞美德拉打敗了帕拉提婆,取回王位。

我想這集最誇張的一幕就是小隊帶著盾牌搭上投石器,以盾牌聚為砲彈,飛躍過王宮城牆,突破城牆防守這一幕了吧。真的是讓人目瞪口呆,腦洞大開,這也就是電影好看的地方啦。

Heroku 的 deploy

以前只知道可以用 git push 來 deploy,今天用 google 找了一下,發現 heroku 已經提供了 container registry,所以現在可以用 docker 來佈署。

所以要先寫好 Dockerfile,到時候會方便很多。

MacOS 換動態桌布

MacOS 可以換動態桌布,會隨著時間而變化,這真的很酷。

透過免費資源網路社群的這篇:Dynamic Wallpaper Club 提供 macOS 動態桌面下載,依時間顯示不同效果 知道了 Dynamic Wallpaper Club 這個收錄許多動態桌布的網站,就上去找,真的有很多。

在換的時候有個小插曲,因為我接了外接螢幕,所以就在想怎麼換另外一個螢幕的桌布,可是一直找都沒找到。後來才發現,當進入「系統偏好設定」>「桌面與螢幕保護程式」時,兩個螢幕同時都會出現「桌面與螢幕保護程式」視窗,這時候就可以去個別指定了。

電影流水帳(2020/03/16~2020/03/25)

sophie turner
Sophie Turner

Dark Phoneix

之前同事已經去電影院看過了,他覺得很可惜,沒能好好發揮。我看過之後的感覺跟他一樣,就是可惜。原本應該是個好題材,卻被糟蹋了。

X-Men 由 Raven 帶領,帶著 Jean, Cyclops 等人一同上太空救人,大部分的人都被會瞬移的 Kurt 救到了,但隊長在別的位置,所以沒有。Raven 原本打算就此打道回府,可是 Professor X 堅持要救到所有人,Raven 拗不過 Professor X ,派了 Jean 過去撐住船體,由 Kurt 再去找人。眼看不明能量就要擊中船體,大家都非常緊張,可是 Kurt 順利完成任務,救回隊長。只是 Jean 呢?Jean 來不及,硬是被不明能量擊中。大家以為 Jean 已經犧牲了,過了沒多久,Jean 居然還活著,趕緊把她救回來。

回到地球以後,Raven 不爽 Professor X,她覺得 Professor X 變了,太在乎名聲,太在乎人類。Jean 則是沒有大礙,檢查時,只發現了體內的能量活動異常的高。檢查完,就去參加慶功派對了。在派對上,Jean 好像想起了什麼,整個能量大爆炸。Jean 就這樣離開,去找他的生父。之後,Professor X 才解釋他在 Jean 小時候做了一件事情,因為 Jean 小時候因為超能力意外害死了母親,Professor X 為了讓他能走過去,利用超能力掩蓋了她這段記憶。Jean 找到父親以後,跟父親簡短的交談,這讓她想起了自己所做的事情,她感到痛苦。這時候 Professor X / Beast / Raven 等人也趕到,想要帶回 Jean ,在溝通失敗的情況下,雙方起了衝突,這場衝突導致了 Raven 意外死去。Jean 感到很抱歉,就走了。Beast 不能釋懷 Professor X 的作為,他只想要找到 Jean ,讓她罪有應得。

Jean 跑去找了 Magneto ,想找他聊聊。此時的 Magneto 還不知道 Raven 的事情,只跟 Jean 聊了幾句話,軍方就趕來追捕了。Jean 起了殺心,想把來追捕的人都幹掉,Magneto 阻止了 Jean,並要軍方跟 Jean 都離開。Jean 離開了,軍方也離開了,Beast 這時找上了 Magneto ,說了 Raven 的事情。這下可好,兩個都曾愛上 Raven 的男人,聯手想為 Raven 復仇,就一同去追趕 Jean。這時候 Professor X 也找了Cyclops 等人,想要阻止 Beast ,拯救 Jean。因為意外殺人而內疚的 Jean ,沒有人聽她的話,反倒是跟隨那股不明能量的外星人找到了她,並用言語引誘她送出力量。Jean 萬念俱灰,就把力量給了外星人。Professor X 等人趕到,問出了事情的情況,趕來的 Beast 跟 Magneto 也聽到了,至此轉變成大家一起幫助 Jean。Jean 覺醒了,知道自己力量不該給外星人,就重新拿回力量,打敗了外星人。後面結局有點忘記了,忘記 Jean 後來如何,只記得 Professor X 深切的反省了自己對 Jean 所做的事情。

サバイバルファミリー

生存家族,這個蠻有意思的電影。

故事從一個家庭的日常開始,爸爸是個典型的上班族,上班認真,在家就是廢,所以跟孩子的感情也不好。媽媽是典型的好媽媽,照料家裡,但管不太動孩子。哥哥個性內向,很靜,每天就是聽音樂,不太跟家人交談。妹妹有點叛逆,只想跟學校的姊妹淘打好關係,在家愛理不理的,看不太起自己的父親。這一天,爸爸起床發現已經8點多了,他搖了搖鬧鐘,發現指針沒動,以為是忘了換電池,想說糟了,趕緊匆忙的起身。他趕忙叫了孩子跟妻子,匆忙的準備,當然也來不及吃早餐。媽媽則是趕著做早餐,她發現怪怪的,以為是停電。爸爸跟哥哥去重新啟動總電源,但沒有用。就這樣,媽媽早餐也來不及做,爸爸跟孩子們就匆忙的出門了。在通勤的路途上,他們都遭遇了困難,交通運輸都不通。好不容易趕到學校,只說是大規模停電,公司跟學校只能屈就於沒有電力的情況下去上班跟上課。媽媽在家也遭遇了沒水,跟採買結帳速度很慢的情況。

這樣的情況持續了兩三天,公司的主管宣佈停止上班,學校也宣佈了停課。爸爸回家跟媽媽商量以後,決定去投靠在鹿兒島的岳父。妹妹發了很多牢騷,覺得為什麼要放棄這麼便利的生活去鄉下的鹿兒島,但也只能屈就於目前的情況,遵從父母的想法。全家把該帶的衣服、行李跟錢糧打包好以後,就出發了,首先先去成田機場探一探,看能不能搭飛機。四人騎了腳踏車到機場,想當然爾,機場封閉。四人無奈,只能改走高速公路去鹿兒島。失去電力以後,整個日本陷入混亂的狀態,拿大把的錢還不一定換的到食物跟水,四人只能將就的前進。之後就是講他們的歷程,碰到暴風雨,碰到沒水、沒食物的各種情況,四人的相處因此慢慢有了改變,也更加瞭解彼此。最終他們還是到了鹿兒島,順利投靠了岳父。時間匆匆過了三年,電力突然回復了,他們回到東京,但他們在這三年裡學會了了解彼此,學會了怎麼過沒有電力的生活。

筆記:Firebase functions with authentication

主要可以 trigger 兩個,一個是 onCreate,一個是 onDelete

onCreate 在以下情況會被觸發

  • A user creates an email account and password.
  • A user signs in for the first time using a federated identity provider.
  • The developer creates an account using the Firebase Admin SDK.
  • A user signs in to a new anonymous auth session for the first time.
exports.sendWelcomeEmail = functions.auth.user().onCreate((user) => {
  // ...
});


onDelete 在使用者被刪除時觸發

exports.sendByeEmail = functions.auth.user().onDelete((user) => {
  // ...
});

裡面的 user 主要就是 UserRecord

email / displayName / customClaims / uid / …. 等欄位都有。

onDelete 可以用來做刪除 firestore/realtime database 上的資料,需要考慮 functions 能執行多久。

修改 git commit 的 author name

因為 commit 時的作者身份錯亂,所以要改掉。在 StackOverflow 查到這篇:How to change the commit author for one specific commit?

  1. 先 git rebase -i ,然後把 pick 全改成 edit
  2. 接著,git 會切到第一個 commit 結束的時間點
  3. 輸入 git commit –amend —author=”Author Name <email@address.com>” –no-edit ,這樣就會把這次 commit 的 author 修改為 Author Name <email@address.com> 。對,別忘了,這邊要改成你自己的名字跟 e-mail 。
  4. 再輸入 git rebase –continue 切到下個 commit
  5. 就這樣依序作業直到結束。

很麻煩,如果有上千個 commit 的話….(眼神死),所以以後還是要把 author 設定好,否則又要再苦一次。

axios catch

一般是寫 .catch((err) => {console.log(err);}

這樣只會看到 http status 的錯誤,不能取得 response。那該怎麼取得 error response 呢?axios 文件有寫了,用 err.response: https://github.com/axios/axios#handling-errors

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);    // 取得內容,用 error.response.data
      console.log(error.response.status);  // 取得狀態碼,用 error.response.status
      console.log(error.response.headers); // 取得表頭,用 error.response.headers
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
    console.log(error.config);
  });