Skip to content

Instantly share code, notes, and snippets.

@hankei6km
Last active January 23, 2024 09:42
Show Gist options
  • Save hankei6km/ac0324db986be1a6d16000f4e6f8e93a to your computer and use it in GitHub Desktop.
Save hankei6km/ac0324db986be1a6d16000f4e6f8e93a to your computer and use it in GitHub Desktop.
「Gemini API を使いスマホでスキャンした表やグラフから自動的にスプレッドシートを作ってみる」用の.gs ソース。(https://zenn.dev/hankei6km/articles/scanned-tables-to-spreadsheets-with-gemini-gas)
function writeToSheet_(destFolder, fileName, values) {
const fileList = destFolder.getFilesByName(fileName)
let ssFile = null
//console.log(fileName)
if (fileList.hasNext()) {
ssFile = SpreadsheetApp.openById(fileList.next().getId())
} else {
ssFile = SpreadsheetApp.create(fileName)
DriveApp.getFileById(ssFile.getId()).moveTo(destFolder)
}
ssFile.getActiveSheet().getRange(1, 1, values.length, values[0].length).setValues(values)
}
function scan_(apiKey, destFolder, file) {
const ssFileName = file.getName().split('.').slice(0, -1).join('.')
const descripton = file.getDescription()
if (descripton === undefined || descripton === null || descripton === '') {
file.setDescription('scaning...')
const [kind, values, body] = ScanImageWithGemini.scan(apiKey, file.getId())
if (kind === ' ## 表が含まれている画像' || kind === ' ## グラフが含まれている画像') {
writeToSheet_(destFolder, ssFileName, values)
}
file.setDescription(body.substring(0, 400))
}
}
function scan(changeList) {
const props = PropertiesService.getScriptProperties()
const apiKey = props.getProperty('GEMINI_API_KEY')
const destFolder = DriveApp.getFolderById(props.getProperty('DEST_FOLDER'))
const list = Object.assign({}, changeList)
for (const item of list.items || []) {
if (item.file) {
if ((item.file.description === undefined || item.file.description === '') && item.file.parents?.some((p) => p.id && p.id === (props.getProperty('SRC_FOLDER')))) {
scan_(apiKey, destFolder, DriveApp.getFileById(item.file.id))
}
}
}
}
function start() {
const props = PropertiesService.getScriptProperties()
const apiKey = props.getProperty('GEMINI_API_KEY')
const srcFolder = DriveApp.getFolderById(props.getProperty('SRC_FOLDER'))
const destFolder = DriveApp.getFolderById(props.getProperty('DEST_FOLDER'))
const files = srcFolder.getFiles()
while (files.hasNext()) {
scan_(apiKey, destFolder, files.next())
}
}
const PROMPT_ = `画像の内容について。
以下の中から最も適切な方法で説明してください。
## 表が含まれている画像の場会
返答の最初に「## 表が含まれている画像」と記述したあと、表の内容をマークダウン形式で記述してください。
## グラフが含まれている画像の場合
返答の最初に「## グラフが含まれている画像」と記述したあと、グラフの内容をマークダウンの表として記述してください。
## テキスト(文章または簡単な説明文など)が含まれている画像の場合
返答の最初に「## テキストが含まれている画像」と記述したあと、テキストの内容を記述してください。
## その他の画像の場合
返答の最初に「## その他の画像」と記述したあと、画像の説明を記述してください。
`
function scan_(apiKey, prompt, srcFile) {
const fileThumb = srcFile.getThumbnail()
const baseImage = Utilities.base64Encode(fileThumb.getBytes());
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${apiKey}`
, payload = {
'contents': [
{
'parts': [{
"inline_data": {
"mime_type": "image/jpeg",
"data": baseImage.toString()
}
}, {
'text': prompt
}]
}
],
/* "generationConfig": {
"stopSequences": [
"Title"
],
"temperature": 0.0,
"maxOutputTokens": 800,
"topP": 0.0,
"topK": 1
} */
}
, options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(payload)
};
const res = UrlFetchApp.fetch(url, options)
, resJson = JSON.parse(res.getContentText());
return resJson.candidates[0].content.parts[0].text
}
function formatBody_(kind, body_tmp) {
if (kind.match(/^ ## /)) {
let lines = body_tmp.slice(body_tmp.findIndex(l => l !== ''))
if (kind === ' ## 表が含まれている画像' || kind === ' ## グラフが含まれている画像') {
return lines.map(l => l.replace(/(^\|)|(\|$)/g, "")).join('\n')
}
return lines.join('\n')
}
return [kind, ...body_tmp].join('\n')
}
function scan(apiKey, fileId, prompt = '') {
const srcFile = DriveApp.getFileById(fileId)
const text = scan_(apiKey, prompt || PROMPT_, srcFile)
const [kind, ...body_tmp] = text.split('\n')
const body = formatBody_(kind, body_tmp)
if (kind === ' ## 表が含まれている画像' || kind === ' ## グラフが含まれている画像') {
return [kind, Utilities.parseCsv(body, "|"), body]
}
return [kind, [[body]], body]
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('スキャン');
menu.addItem('表としてスキャン', 'onScanTable_');
menu.addItem('プロンプト指定でスキャン', 'onScanCustom_');
menu.addToUi();
}
function onScanTable_() {
const r = SpreadsheetApp.getActiveRange()
const fileId = r.getValue().split('/')[5]
if (fileId) {
const props = PropertiesService.getScriptProperties();
const [kind, values] = ScanImageWithGemini.scan(props.getProperty('GEMINI_API_KEY'), fileId)
const s = SpreadsheetApp.getActiveSheet()
s.getRange(r.getRowIndex() + 1, r.getColumn(), values.length, values[0].length).setValues(values)
}
}
function onScanCustom_() {
const r = SpreadsheetApp.getActiveRange()
const fileId = r.getValue().split('/')[5]
const prompt = SpreadsheetApp.getActiveSheet().getRange(r.getRowIndex() + 1, r.getColumn()).getValue()
if (fileId && prompt) {
const props = PropertiesService.getScriptProperties();
const [kind, values] = ScanImageWithGemini.scan(props.getProperty('GEMINI_API_KEY'), fileId, prompt)
const s = SpreadsheetApp.getActiveSheet()
s.getRange(r.getRowIndex() + 2, r.getColumn(), values.length, values[0].length).setValues(values)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment