Skip to content

Commit 143e827

Browse files
authored
Misc changes before 2.0.0 release (#19)
* add back static-dir flag * staticDir * Implement --write-template * Add general keyboard shortcuts
1 parent a6383b9 commit 143e827

File tree

5 files changed

+130
-61
lines changed

5 files changed

+130
-61
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ to each image on your local file system. The results will go in `output.csv`.
7575

7676
For more details, run `classify-images --help`.
7777

78+
Tips & Tricks
79+
-------------
80+
81+
It can be hard to remember the exact format for template files. localturk can help! Run it with
82+
the `--write-template` argument to generate a template file for your input that you can edit:
83+
84+
localturk --write-template tasks.csv > template.html
85+
86+
When you're going through many tasks, keyboard shortcuts can speed things up tremendously.
87+
localturk supports these via the `data-key` attribute on form elements. For example, make yourer
88+
submit button look like this:
89+
90+
<input type="submit" name="result" value="Good" data-key="d">
91+
92+
Now, when you press `d`, it'll automatically click the "Good" button for you. _Note that this
93+
feature is not available on mechanical turk itself!_
94+
7895
Development
7996
-----------
8097

classify-images.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import * as escape from 'escape-html';
1818
import * as fs from 'fs';
1919
import * as program from 'commander';
2020

21+
import {dedent} from './utils';
22+
2123
const temp = require('temp').track();
2224

2325
function list(val) {
@@ -41,10 +43,11 @@ if (program.args.length == 0) {
4143
}
4244

4345
if (fs.existsSync(program.output)) {
44-
console.warn(`Output file ${program.output} already exists.
45-
Its contents will be assumed to be previously-generated labels.
46-
If you want to start from scratch, either delete this file,
47-
rename it or specify a different output via --output`);
46+
console.warn(dedent`
47+
Output file ${program.output} already exists.
48+
Its contents will be assumed to be previously-generated labels.
49+
If you want to start from scratch, either delete this file,
50+
rename it or specify a different output via --output`);
4851
}
4952

5053
const csvInfo = temp.openSync({suffix: '.csv'});
@@ -55,47 +58,27 @@ fs.closeSync(csvInfo.fd);
5558

5659
const buttonsHtml = program.labels.map((label, idx) => {
5760
const buttonText = `${label} (${1 + idx})`;
58-
return `<button type="submit" id='${1+idx}' name="label" value="${label}">${escape(buttonText)}</button>`;
61+
return `<button type="submit" data-key='${1+idx}' name="label" value="${label}">${escape(buttonText)}</button>`;
5962
}).join('&nbsp;');
6063

6164
const widthHtml = program.max_width ? ` width="${program.max_width}"` : '';
62-
const undoHtml = `
63-
</form>
64-
<form action="/delete-last" method="POST" style="display: inline-block">
65-
<input type="submit" id="undo-button" value="Undo Last (z)">
66-
</form>`;
65+
const undoHtml = dedent`
66+
</form>
67+
<form action="/delete-last" method="POST" style="display: inline-block">
68+
<input type="submit" id="undo-button" data-key="z" value="Undo Last (z)">
69+
</form>`;
6770
let html = buttonsHtml + undoHtml + '\n<p><img src="${path}" ' + widthHtml + '></p>';
6871

6972
// Add keyboard shortcuts. 1=first button, etc.
70-
html += `
71-
<script>
72-
window.addEventListener("keydown", function(e) {
73-
var code = e.keyCode;
74-
if (code == 90) {
75-
var el = document.getElementById("undo-button");
76-
e.preventDefault();
77-
el.click();
78-
return;
79-
}
80-
var num = null;
81-
if (code >= 48 && code <= 57) num = code - 48; // numbers above keyboard
82-
if (code >= 96 && code <= 105) num = code - 96; // numpad
83-
if (num === null) return;
84-
var el = document.getElementById(num);
85-
if (el) {
86-
e.preventDefault();
87-
el.click();
88-
}
89-
});
90-
</script>
91-
<style>
92-
form { display: inline-block; }
93-
#undo-button { margin-left: 20px; }
94-
</style>`;
73+
html += dedent`
74+
<style>
75+
form { display: inline-block; }
76+
#undo-button { margin-left: 20px; }
77+
</style>`;
9578

9679
fs.writeSync(templateInfo.fd, html);
9780
fs.closeSync(templateInfo.fd);
9881

99-
const args = ['localturk', '-q', '--static_dir', '.', templateInfo.path, csvInfo.path, program.output];
82+
const args = ['localturk', '--static-dir', '.', templateInfo.path, csvInfo.path, program.output];
10083
console.log('Running ', args.join(' '));
10184
child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'});

localturk.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,35 @@ import open = require('open');
1818
import * as _ from 'lodash';
1919

2020
import * as csv from './csv';
21+
import {makeTemplate} from './sample-template';
2122
import * as utils from './utils';
2223
import { outputFile } from 'fs-extra';
2324

2425
program
2526
.version('2.0.0')
2627
.usage('[options] template.html tasks.csv outputs.csv')
2728
.option('-p, --port <n>', 'Run on this port (default 4321)', parseInt)
29+
.option('-s, --static-dir <dir>',
30+
'Serve static content from this directory. Default is same directory as template file.')
2831
.option('-w, --write-template', 'Generate a stub template file based on the input CSV.')
2932
.parse(process.argv);
3033

31-
const {args} = program;
32-
if (3 !== args.length) {
34+
const {args, writeTemplate} = program;
35+
if (!((3 === args.length && !writeTemplate) ||
36+
(1 === args.length && writeTemplate))) {
3337
program.help();
3438
}
39+
if (writeTemplate) {
40+
// tasks.csv is the only input with --write-template.
41+
args.unshift('');
42+
args.push('');
43+
}
3544

3645
const [templateFile, tasksFile, outputsFile] = args;
3746
const port = program.port || 4321;
47+
// --static-dir is particularly useful for classify-images, where the template file is in a
48+
// temporary directory but the image files could be anywhere.
49+
const staticDir = program['staticDir'] || path.dirname(templateFile);
3850

3951
type Task = {[key: string]: string};
4052
let flash = ''; // this is used to show warnings in the web UI.
@@ -45,6 +57,7 @@ async function renderTemplate({task, numCompleted, numTotal}: TaskStats) {
4557
for (const k in task) {
4658
fullDict[k] = utils.htmlEntities(task[k]);
4759
}
60+
// Note: these two fields are not available in mechanical turk.
4861
fullDict['ALL_JSON'] = utils.htmlEntities(JSON.stringify(task, null, 2));
4962
fullDict['ALL_JSON_RAW'] = JSON.stringify(task);
5063
const userHtml = utils.renderTemplate(template, fullDict);
@@ -56,19 +69,31 @@ async function renderTemplate({task, numCompleted, numTotal}: TaskStats) {
5669
`<input type=hidden name="${k}" value="${utils.htmlEntities(v)}">`
5770
).join('\n');
5871

59-
return `
60-
<!doctype html>
61-
<html>
62-
<title>${numCompleted} / ${numTotal} - localturk</title>
63-
<body><form action=/submit method=post>
64-
<p>${numCompleted} / ${numTotal} <span style="background: yellow">${thisFlash}</span></p>
65-
${sourceInputs}
66-
${userHtml}
67-
<hr/><input type=submit />
68-
</form>
69-
</body>
70-
</html>
71-
`;
72+
return utils.dedent`
73+
<!doctype html>
74+
<html>
75+
<title>${numCompleted} / ${numTotal} - localturk</title>
76+
<body><form action=/submit method=post>
77+
<p>${numCompleted} / ${numTotal} <span style="background: yellow">${thisFlash}</span></p>
78+
${sourceInputs}
79+
${userHtml}
80+
<hr/><input type=submit />
81+
</form>
82+
<script>
83+
// Support keyboard shortcuts via, e.g. <.. data-key="1" />
84+
window.addEventListener("keydown", function(e) {
85+
if (document.activeElement !== document.body) return;
86+
var key = e.key;
87+
const el = document.querySelector('[data-key="' + key + '"]');
88+
if (el) {
89+
e.preventDefault();
90+
el.click();
91+
}
92+
});
93+
</script>
94+
</body>
95+
</html>
96+
`;
7297
}
7398

7499
async function readCompletedTasks(): Promise<Task[]> {
@@ -119,15 +144,10 @@ async function getNextTask(): Promise<TaskStats> {
119144
}
120145
}
121146

122-
if (program['write-template']) {
123-
// TODO(danvk): implement.
124-
process.exit(0);
125-
}
126-
127147
const app = express();
128148
app.use(bodyParser.urlencoded({extended: false}));
129149
app.use(errorhandler());
130-
app.use(express.static(path.resolve(path.dirname(templateFile))));
150+
app.use(express.static(path.resolve(staticDir)));
131151

132152
app.get('/', utils.wrapPromise(async (req, res) => {
133153
const nextTask = await getNextTask();
@@ -155,7 +175,17 @@ app.post('/delete-last', utils.wrapPromise(async (req, res) => {
155175
res.redirect('/');
156176
}));
157177

158-
app.listen(port);
159-
const url = `http://localhost:${port}`;
160-
console.log('Running local turk on', url);
161-
open(url);
178+
179+
if (writeTemplate) {
180+
(async () => {
181+
const columns = await csv.readHeaders(tasksFile);
182+
console.log(makeTemplate(columns));
183+
})().catch(e => {
184+
console.error(e);
185+
});
186+
} else {
187+
app.listen(port);
188+
const url = `http://localhost:${port}`;
189+
console.log('Running local turk on', url);
190+
open(url);
191+
}

sample-template.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {dedent, htmlEntities} from './utils';
2+
3+
/**
4+
* Write out a sample template file for a given input CSV.
5+
*/
6+
export function makeTemplate(columnNames: string[]) {
7+
const inputs = columnNames.map(column => column + ': ${' + column + '}');
8+
return dedent`
9+
${inputs.join('<br>\n ')}
10+
11+
<!--
12+
Use named form elements to generate output as desired.
13+
Use data-key="x" to set a keyboard shortcut for buttons.
14+
-->
15+
<input type="text" size="80" name="notes" placeholder="Notes go here">
16+
<input type="submit" name="result" data-key="a" value="Class A">
17+
<input type="submit" name="result" data-key="b" value="Class B">`;
18+
}

utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,24 @@ export function wrapPromise(
5353
});
5454
};
5555
}
56+
57+
/**
58+
* Removes leading indents from a template string without removing all leading whitespace.
59+
* Taken from tslint.
60+
*/
61+
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
62+
let fullString = strings.reduce((accumulator, str, i) => accumulator + values[i - 1] + str);
63+
64+
// match all leading spaces/tabs at the start of each line
65+
const match = fullString.match(/^[ \t]*(?=\S)/gm);
66+
if (!match) {
67+
// e.g. if the string is empty or all whitespace.
68+
return fullString;
69+
}
70+
71+
// find the smallest indent, we don't want to remove all leading whitespace
72+
const indent = Math.min(...match.map(el => el.length));
73+
const regexp = new RegExp('^[ \\t]{' + indent + '}', 'gm');
74+
fullString = indent > 0 ? fullString.replace(regexp, '') : fullString;
75+
return fullString;
76+
}

0 commit comments

Comments
 (0)